From e917cb706a2e128420580a8d3f086911e1d30cf6 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Tue, 14 Mar 2023 15:47:58 +0100 Subject: [PATCH 01/12] FEATURE: Add creation and modification timestamps to nodes Resolves: #4092 --- .../DoctrineDbalContentGraphProjection.php | 91 ++++++++++++------- .../DoctrineDbalContentGraphSchemaBuilder.php | 10 ++ .../Projection/Feature/NodeVariation.php | 25 +++-- .../src/Domain/Projection/NodeRecord.php | 31 ++++++- .../src/Domain/Repository/NodeFactory.php | 4 + .../src/Domain/Repository/NodeFactory.php | 4 + .../Classes/Projection/ContentGraph/Node.php | 6 +- .../Functional/Fusion/NodeHelperTest.php | 8 +- 8 files changed, 129 insertions(+), 50 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 2ce39ca4e8f..55a44ec5353 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -203,39 +203,39 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch $catchUpHook->onBeforeEvent($eventInstance, $eventEnvelope); if ($eventInstance instanceof RootNodeAggregateWithNodeWasCreated) { - $this->whenRootNodeAggregateWithNodeWasCreated($eventInstance); + $this->whenRootNodeAggregateWithNodeWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWithNodeWasCreated) { - $this->whenNodeAggregateWithNodeWasCreated($eventInstance); + $this->whenNodeAggregateWithNodeWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateNameWasChanged) { - $this->whenNodeAggregateNameWasChanged($eventInstance); + $this->whenNodeAggregateNameWasChanged($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof ContentStreamWasForked) { $this->whenContentStreamWasForked($eventInstance); } elseif ($eventInstance instanceof ContentStreamWasRemoved) { $this->whenContentStreamWasRemoved($eventInstance); } elseif ($eventInstance instanceof NodePropertiesWereSet) { - $this->whenNodePropertiesWereSet($eventInstance); + $this->whenNodePropertiesWereSet($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeReferencesWereSet) { - $this->whenNodeReferencesWereSet($eventInstance); + $this->whenNodeReferencesWereSet($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWasEnabled) { - $this->whenNodeAggregateWasEnabled($eventInstance); + $this->whenNodeAggregateWasEnabled($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateTypeWasChanged) { - $this->whenNodeAggregateTypeWasChanged($eventInstance); + $this->whenNodeAggregateTypeWasChanged($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof DimensionSpacePointWasMoved) { $this->whenDimensionSpacePointWasMoved($eventInstance); } elseif ($eventInstance instanceof DimensionShineThroughWasAdded) { $this->whenDimensionShineThroughWasAdded($eventInstance); } elseif ($eventInstance instanceof NodeAggregateWasRemoved) { - $this->whenNodeAggregateWasRemoved($eventInstance); + $this->whenNodeAggregateWasRemoved($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWasMoved) { - $this->whenNodeAggregateWasMoved($eventInstance); + $this->whenNodeAggregateWasMoved($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeSpecializationVariantWasCreated) { - $this->whenNodeSpecializationVariantWasCreated($eventInstance); + $this->whenNodeSpecializationVariantWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeGeneralizationVariantWasCreated) { - $this->whenNodeGeneralizationVariantWasCreated($eventInstance); + $this->whenNodeGeneralizationVariantWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodePeerVariantWasCreated) { - $this->whenNodePeerVariantWasCreated($eventInstance); + $this->whenNodePeerVariantWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWasDisabled) { - $this->whenNodeAggregateWasDisabled($eventInstance); + $this->whenNodeAggregateWasDisabled($eventInstance, $eventEnvelope); } else { throw new \RuntimeException('Not supported: ' . get_class($eventInstance)); } @@ -272,8 +272,10 @@ public function markStale(): void /** * @throws \Throwable */ - private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event): void + private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { + $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; + $nodeRelationAnchorPoint = NodeRelationAnchorPoint::create(); $dimensionSpacePoint = DimensionSpacePoint::fromArray([]); $node = new NodeRecord( @@ -283,7 +285,12 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo $dimensionSpacePoint->hash, SerializedPropertyValues::fromArray([]), $event->nodeTypeName, - $event->nodeAggregateClassification + $event->nodeAggregateClassification, + null, + $eventEnvelope->recordedAt, + $initiatingDateTime, + null, + null, ); $this->transactional(function () use ($node, $event) { @@ -301,9 +308,9 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo /** * @throws \Throwable */ - private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event): void + private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { $this->createNodeWithHierarchy( $event->contentStreamId, $event->nodeAggregateId, @@ -314,7 +321,8 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre $event->initialPropertyValues, $event->nodeAggregateClassification, $event->succeedingNodeAggregateId, - $event->nodeName + $event->nodeName, + $eventEnvelope, ); $this->connectRestrictionRelationsFromParentNodeToNewlyCreatedNode( @@ -402,10 +410,12 @@ private function createNodeWithHierarchy( DimensionSpacePointSet $visibleInDimensionSpacePoints, SerializedPropertyValues $propertyDefaultValuesAndTypes, NodeAggregateClassification $nodeAggregateClassification, - NodeAggregateId $succeedingSiblingNodeAggregateId = null, - NodeName $nodeName = null + ?NodeAggregateId $succeedingSiblingNodeAggregateId, + ?NodeName $nodeName, + EventEnvelope $eventEnvelope, ): void { $nodeRelationAnchorPoint = NodeRelationAnchorPoint::create(); + $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; $node = new NodeRecord( $nodeRelationAnchorPoint, $nodeAggregateId, @@ -414,7 +424,11 @@ private function createNodeWithHierarchy( $propertyDefaultValuesAndTypes, $nodeTypeName, $nodeAggregateClassification, - $nodeName + $nodeName, + $eventEnvelope->recordedAt, + $initiatingDateTime, + null, + null, ); // reconnect parent relations @@ -463,7 +477,7 @@ private function createNodeWithHierarchy( * @param DimensionSpacePointSet $dimensionSpacePointSet * @throws \Doctrine\DBAL\DBALException */ - protected function connectHierarchy( + private function connectHierarchy( ContentStreamId $contentStreamId, NodeRelationAnchorPoint $parentNodeAnchorPoint, NodeRelationAnchorPoint $childNodeAnchorPoint, @@ -503,7 +517,7 @@ protected function connectHierarchy( * @return int * @throws \Doctrine\DBAL\DBALException */ - protected function getRelationPosition( + private function getRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, @@ -540,7 +554,7 @@ protected function getRelationPosition( * @return int * @throws \Doctrine\DBAL\DBALException */ - protected function getRelationPositionAfterRecalculation( + private function getRelationPositionAfterRecalculation( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, @@ -694,11 +708,14 @@ public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): voi /** * @throws \Throwable */ - public function whenNodePropertiesWereSet(NodePropertiesWereSet $event): void + public function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { - $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { + $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties->merge($event->propertyValues); + $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; + $node->lastModifiedAt = $eventEnvelope->recordedAt; + $node->originalLastModifiedAt = $initiatingDateTime; }); }); } @@ -906,8 +923,10 @@ protected function copyHierarchyRelationToDimensionSpacePoint( */ protected function copyNodeToDimensionSpacePoint( NodeRecord $sourceNode, - OriginDimensionSpacePoint $originDimensionSpacePoint + OriginDimensionSpacePoint $originDimensionSpacePoint, + EventEnvelope $eventEnvelope, ): NodeRecord { + $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; $copyRelationAnchorPoint = NodeRelationAnchorPoint::create(); $copy = new NodeRecord( $copyRelationAnchorPoint, @@ -917,7 +936,11 @@ protected function copyNodeToDimensionSpacePoint( $sourceNode->properties, $sourceNode->nodeTypeName, $sourceNode->classification, - $sourceNode->nodeName + $sourceNode->nodeName, + $eventEnvelope->recordedAt, + $initiatingDateTime, + null, + null, ); $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); @@ -948,7 +971,7 @@ function (NodeRecord $node) use ($event) { * @throws \Doctrine\DBAL\DBALException * @throws \Exception */ - protected function updateNodeWithCopyOnWrite(EventInterface $event, callable $operations): mixed + private function updateNodeWithCopyOnWrite(EventInterface $event, callable $operations): mixed { if ( method_exists($event, 'getNodeAggregateId') @@ -986,7 +1009,7 @@ protected function updateNodeWithCopyOnWrite(EventInterface $event, callable $op ); } - protected function updateNodeRecordWithCopyOnWrite( + private function updateNodeRecordWithCopyOnWrite( ContentStreamId $contentStreamIdWhereWriteOccurs, NodeRelationAnchorPoint $anchorPoint, callable $operations @@ -1048,7 +1071,7 @@ protected function updateNodeRecordWithCopyOnWrite( } - protected function copyReferenceRelations( + private function copyReferenceRelations( NodeRelationAnchorPoint $sourceRelationAnchorPoint, NodeRelationAnchorPoint $destinationRelationAnchorPoint ): void { @@ -1211,12 +1234,12 @@ public function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded /** * @throws \Throwable */ - protected function transactional(\Closure $operations): void + private function transactional(\Closure $operations): void { $this->getDatabaseConnection()->transactional($operations); } - protected function getDatabaseConnection(): Connection + private function getDatabaseConnection(): Connection { return $this->dbalClient->getConnection(); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 0fa11486831..94774a77869 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -50,6 +50,16 @@ private function createNodeTable(Schema $schema): void $table->addColumn('classification', Types::STRING) ->setLength(255) ->setNotnull(true); + $table->addColumn('createdat', Types::DATETIME_IMMUTABLE) + ->setNotnull(true); + $table->addColumn('originalcreatedat', Types::DATETIME_IMMUTABLE) + ->setNotnull(true); + $table->addColumn('lastmodifiedat', Types::DATETIME_IMMUTABLE) + ->setNotnull(false) + ->setDefault(null); + $table->addColumn('originallastmodifiedat', Types::DATETIME_IMMUTABLE) + ->setNotnull(false) + ->setDefault(null); $table ->setPrimaryKey(['relationanchorpoint']) ->addIndex(['nodeaggregateid'], 'NODE_AGGREGATE_ID') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 23d888114e5..d1ec99a1369 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\EventStore\Model\EventEnvelope; /** * The NodeVariation projection feature trait @@ -35,9 +36,9 @@ abstract protected function getTableNamePrefix(): string; * @throws \Exception * @throws \Throwable */ - private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event): void + private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { // Do the actual specialization $sourceNode = $this->getProjectionContentGraph()->findNodeInAggregate( $event->contentStreamId, @@ -50,7 +51,8 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $specializedNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, - $event->specializationOrigin + $event->specializationOrigin, + $eventEnvelope ); $uncoveredDimensionSpacePoints = $event->specializationCoverage->points; @@ -136,9 +138,9 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria * @throws \Exception * @throws \Throwable */ - public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event): void + public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { // do the generalization $sourceNode = $this->getProjectionContentGraph()->findNodeInAggregate( $event->contentStreamId, @@ -158,7 +160,8 @@ public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVarian } $generalizedNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, - $event->generalizationOrigin + $event->generalizationOrigin, + $eventEnvelope ); $unassignedIngoingDimensionSpacePoints = $event->generalizationCoverage; @@ -244,9 +247,9 @@ public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVarian /** * @throws \Throwable */ - public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event): void + public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { // Do the peer variant creation itself $sourceNode = $this->getProjectionContentGraph()->findNodeInAggregate( $event->contentStreamId, @@ -266,7 +269,8 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event): } $peerNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, - $event->peerOrigin + $event->peerOrigin, + $eventEnvelope ); $unassignedIngoingDimensionSpacePoints = $event->peerCoverage; @@ -336,7 +340,8 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event): abstract protected function copyNodeToDimensionSpacePoint( NodeRecord $sourceNode, - OriginDimensionSpacePoint $originDimensionSpacePoint + OriginDimensionSpacePoint $originDimensionSpacePoint, + EventEnvelope $eventEnvelope, ): NodeRecord; abstract protected function copyHierarchyRelationToDimensionSpacePoint( diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index 58ddd8dc910..f0ae86012be 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -15,6 +15,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Types; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -38,7 +39,11 @@ public function __construct( public NodeTypeName $nodeTypeName, public NodeAggregateClassification $classification, /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ - public ?NodeName $nodeName = null + public ?NodeName $nodeName, + public \DateTimeImmutable $createdAt, + public \DateTimeImmutable $originalCreatedAt, + public ?\DateTimeImmutable $lastModifiedAt, + public ?\DateTimeImmutable $originalLastModifiedAt, ) { } @@ -55,7 +60,16 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP 'origindimensionspacepointhash' => $this->originDimensionSpacePointHash, 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, - 'classification' => $this->classification->value + 'classification' => $this->classification->value, + 'createdat' => $this->createdAt, + 'originalcreatedat' => $this->originalCreatedAt, + 'lastmodifiedat' => $this->lastModifiedAt, + 'originallastmodifiedat' => $this->originalLastModifiedAt, + ], [ + 'createdat' => Types::DATETIME_IMMUTABLE, + 'originalcreatedat' => Types::DATETIME_IMMUTABLE, + 'lastmodifiedat' => Types::DATETIME_IMMUTABLE, + 'originallastmodifiedat' => Types::DATETIME_IMMUTABLE, ]); } @@ -73,10 +87,15 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa 'origindimensionspacepointhash' => $this->originDimensionSpacePointHash, 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, - 'classification' => $this->classification->value + 'classification' => $this->classification->value, + 'lastmodifiedat' => $this->lastModifiedAt, + 'originallastmodifiedat' => $this->originalLastModifiedAt, ], [ 'relationanchorpoint' => $this->relationAnchorPoint + ], [ + 'lastmodifiedat' => Types::DATETIME_IMMUTABLE, + 'originallastmodifiedat' => Types::DATETIME_IMMUTABLE, ] ); } @@ -107,7 +126,11 @@ public static function fromDatabaseRow(array $databaseRow): self SerializedPropertyValues::fromArray(json_decode($databaseRow['properties'], true)), NodeTypeName::fromString($databaseRow['nodetypename']), NodeAggregateClassification::from($databaseRow['classification']), - isset($databaseRow['name']) ? NodeName::fromString($databaseRow['name']) : null + isset($databaseRow['name']) ? NodeName::fromString($databaseRow['name']) : null, + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['createdat']), + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['originalcreatedat']), + isset($databaseRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['lastmodifiedat']) : null, + isset($databaseRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['originallastmodifiedat']) : null, ); } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index afba73d609a..402ed968b24 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -77,6 +77,10 @@ public function mapNodeRowToNode( $this->nodeTypeManager->getNodeType($nodeRow['nodetypename']), $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['createdat']), + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originalcreatedat']), + isset($nodeRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originallastmodifiedat']) : null, ); } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 851d7c1361e..5132f21159f 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -88,6 +88,10 @@ public function mapNodeRowToNode( $this->propertyConverter ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['createdat']), + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originalcreatedat']), + isset($nodeRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originallastmodifiedat']) : null, ); return $result; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index 935d6530777..b975204c17d 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -67,7 +67,11 @@ public function __construct( * @return PropertyCollectionInterface Property values, indexed by their name */ public readonly PropertyCollectionInterface $properties, - public readonly ?NodeName $nodeName + public readonly ?NodeName $nodeName, + public readonly \DateTimeImmutable $createdAt, + public readonly \DateTimeImmutable $originalCreatedAt, + public readonly ?\DateTimeImmutable $lastModifiedAt, + public readonly ?\DateTimeImmutable $originalLastModifiedAt, ) { } diff --git a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php index 6905f7c8031..8252f34c1d2 100644 --- a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php +++ b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php @@ -148,6 +148,8 @@ protected function setUp(): void return null; }); + $now = new \DateTimeImmutable(); + $this->textNode = new Node( ContentSubgraphIdentity::create( ContentRepositoryId::fromString("cr"), @@ -161,7 +163,11 @@ protected function setUp(): void NodeTypeName::fromString("nt"), $nodeType, $textNodeProperties, - null + null, + $now, + $now, + null, + null, ); } } From 1fa5a422dacb53eb686a233d37c830482fb44bd1 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Wed, 15 Mar 2023 19:11:07 +0100 Subject: [PATCH 02/12] WIP --- .../DoctrineDbalContentGraphProjection.php | 74 ++++++++++++------- .../Projection/Feature/NodeDisabling.php | 4 +- .../Domain/Projection/Feature/NodeMove.php | 4 +- .../src/Domain/Projection/NodeRecord.php | 20 +++-- .../src/Domain/Repository/NodeFactory.php | 17 ++++- .../src/Domain/Repository/NodeFactory.php | 17 ++++- .../Classes/ContentRepository.php | 22 +++++- .../Factory/ContentRepositoryFactory.php | 9 ++- .../Features/Bootstrap/EventSourcedTrait.php | 39 ++++++---- Neos.ContentRepository.Core/composer.json | 3 +- 10 files changed, 149 insertions(+), 60 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 55a44ec5353..bcc768e13cb 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Comparator; +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; @@ -225,7 +226,7 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch } elseif ($eventInstance instanceof DimensionShineThroughWasAdded) { $this->whenDimensionShineThroughWasAdded($eventInstance); } elseif ($eventInstance instanceof NodeAggregateWasRemoved) { - $this->whenNodeAggregateWasRemoved($eventInstance, $eventEnvelope); + $this->whenNodeAggregateWasRemoved($eventInstance); } elseif ($eventInstance instanceof NodeAggregateWasMoved) { $this->whenNodeAggregateWasMoved($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeSpecializationVariantWasCreated) { @@ -274,8 +275,6 @@ public function markStale(): void */ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { - $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; - $nodeRelationAnchorPoint = NodeRelationAnchorPoint::create(); $dimensionSpacePoint = DimensionSpacePoint::fromArray([]); $node = new NodeRecord( @@ -288,7 +287,7 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo $event->nodeAggregateClassification, null, $eventEnvelope->recordedAt, - $initiatingDateTime, + self::initiatingDateTime($eventEnvelope), null, null, ); @@ -337,22 +336,30 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre /** * @throws \Throwable */ - private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event): void + private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { - $this->getDatabaseConnection()->executeUpdate(' + $this->transactional(function () use ($event, $eventEnvelope) { + $this->getDatabaseConnection()->executeStatement(' UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h - inner join ' . $this->tableNamePrefix . '_node n on + INNER JOIN ' . $this->tableNamePrefix . '_node n on h.childnodeanchor = n.relationanchorpoint SET - h.name = :newName + h.name = :newName, + n.lastmodifiedat = :lastModifiedAt, + n.originallastmodifiedat = :originalLastModifiedAt + WHERE n.nodeaggregateid = :nodeAggregateId and h.contentstreamid = :contentStreamId ', [ 'newName' => (string)$event->newNodeName, 'nodeAggregateId' => (string)$event->nodeAggregateId, - 'contentStreamId' => (string)$event->contentStreamId + 'contentStreamId' => (string)$event->contentStreamId, + 'lastModifiedAt' => $eventEnvelope->recordedAt, + 'originalLastModifiedAt' => self::initiatingDateTime($eventEnvelope), + ], [ + 'lastModifiedAt' => Types::DATETIME_IMMUTABLE, + 'originalLastModifiedAt' => Types::DATETIME_IMMUTABLE, ]); }); } @@ -415,7 +422,6 @@ private function createNodeWithHierarchy( EventEnvelope $eventEnvelope, ): void { $nodeRelationAnchorPoint = NodeRelationAnchorPoint::create(); - $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; $node = new NodeRecord( $nodeRelationAnchorPoint, $nodeAggregateId, @@ -426,7 +432,7 @@ private function createNodeWithHierarchy( $nodeAggregateClassification, $nodeName, $eventEnvelope->recordedAt, - $initiatingDateTime, + self::initiatingDateTime($eventEnvelope), null, null, ); @@ -600,7 +606,7 @@ private function getRelationPositionAfterRecalculation( /** * @throws \Throwable */ - public function whenContentStreamWasForked(ContentStreamWasForked $event): void + private function whenContentStreamWasForked(ContentStreamWasForked $event): void { $this->transactional(function () use ($event) { @@ -659,7 +665,7 @@ public function whenContentStreamWasForked(ContentStreamWasForked $event): void }); } - public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void + private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { $this->transactional(function () use ($event) { @@ -708,14 +714,13 @@ public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): voi /** * @throws \Throwable */ - public function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void + private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties->merge($event->propertyValues); - $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; $node->lastModifiedAt = $eventEnvelope->recordedAt; - $node->originalLastModifiedAt = $initiatingDateTime; + $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); }); }); } @@ -723,9 +728,9 @@ public function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnv /** * @throws \Throwable */ - public function whenNodeReferencesWereSet(NodeReferencesWereSet $event): void + private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { foreach ($event->affectedSourceOriginDimensionSpacePoints as $originDimensionSpacePoint) { $nodeAnchorPoint = $this->projectionContentGraph ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( @@ -747,7 +752,9 @@ public function whenNodeReferencesWereSet(NodeReferencesWereSet $event): void $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, $nodeAnchorPoint, - function (NodeRecord $node) { + function (NodeRecord $node) use ($eventEnvelope) { + $node->lastModifiedAt = $eventEnvelope->recordedAt; + $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); } ); @@ -871,7 +878,7 @@ private function cascadeRestrictionRelations( /** * @throws \Throwable */ - public function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void + private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event) { $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( @@ -879,6 +886,7 @@ public function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): voi $event->nodeAggregateId, $event->affectedDimensionSpacePoints ); + // TODO update last modified values }); } @@ -926,7 +934,6 @@ protected function copyNodeToDimensionSpacePoint( OriginDimensionSpacePoint $originDimensionSpacePoint, EventEnvelope $eventEnvelope, ): NodeRecord { - $initiatingDateTime = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; $copyRelationAnchorPoint = NodeRelationAnchorPoint::create(); $copy = new NodeRecord( $copyRelationAnchorPoint, @@ -938,7 +945,7 @@ protected function copyNodeToDimensionSpacePoint( $sourceNode->classification, $sourceNode->nodeName, $eventEnvelope->recordedAt, - $initiatingDateTime, + self::initiatingDateTime($eventEnvelope), null, null, ); @@ -947,9 +954,9 @@ protected function copyNodeToDimensionSpacePoint( return $copy; } - public function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event): void + private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( $event->nodeAggregateId, $event->contentStreamId @@ -959,8 +966,10 @@ public function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $eve $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, $anchorPoint, - function (NodeRecord $node) use ($event) { + function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; + $node->lastModifiedAt = $eventEnvelope->recordedAt; + $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); } ); } @@ -1096,7 +1105,7 @@ private function copyReferenceRelations( ]); } - public function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void + private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void { $this->transactional(function () use ($event) { // the ordering is important - we first update the OriginDimensionSpacePoints, as we need the @@ -1170,7 +1179,7 @@ function (NodeRecord $nodeRecord) use ($event) { }); } - public function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded $event): void + private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded $event): void { $this->transactional(function () use ($event) { // 1) hierarchy relations @@ -1243,4 +1252,13 @@ private function getDatabaseConnection(): Connection { return $this->dbalClient->getConnection(); } + + private static function initiatingDateTime(EventEnvelope $eventEnvelope): \DateTimeImmutable + { + $result = $eventEnvelope->event->metadata->has('initiatingTimestamp') ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $eventEnvelope->event->metadata->get('initiatingTimestamp')) : $eventEnvelope->recordedAt; + if (!$result instanceof \DateTimeImmutable) { + throw new \RuntimeException(sprintf('Failed to extract initiating timestamp from event "%s"', $eventEnvelope->event->id->value), 1678902291); + } + return $result; + } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php index bb68b36dd67..0db7469dc92 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; +use Neos\EventStore\Model\EventEnvelope; /** * The NodeDisabling projection feature trait @@ -19,7 +20,7 @@ abstract protected function getTableNamePrefix(): string; /** * @throws \Throwable */ - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void + private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event) { // TODO: still unsure why we need an "INSERT IGNORE" here; @@ -83,6 +84,7 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY ] ); + // TODO update last modified values }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index e76f64d00de..836e46b1ecf 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -20,6 +20,7 @@ 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 @@ -38,7 +39,7 @@ abstract protected function getTableNamePrefix(): string; * @param NodeAggregateWasMoved $event * @throws \Throwable */ - private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event) { foreach ($event->nodeMoveMappings as $moveNodeMapping) { @@ -100,6 +101,7 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void ); } } + // TODO update last modified values }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index f0ae86012be..152df61585a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -93,7 +93,8 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa ], [ 'relationanchorpoint' => $this->relationAnchorPoint - ], [ + ], + [ 'lastmodifiedat' => Types::DATETIME_IMMUTABLE, 'originallastmodifiedat' => Types::DATETIME_IMMUTABLE, ] @@ -127,10 +128,19 @@ public static function fromDatabaseRow(array $databaseRow): self NodeTypeName::fromString($databaseRow['nodetypename']), NodeAggregateClassification::from($databaseRow['classification']), isset($databaseRow['name']) ? NodeName::fromString($databaseRow['name']) : null, - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['createdat']), - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['originalcreatedat']), - isset($databaseRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['lastmodifiedat']) : null, - isset($databaseRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $databaseRow['originallastmodifiedat']) : null, + self::parseDateTimeString($databaseRow['createdat']), + self::parseDateTimeString($databaseRow['originalcreatedat']), + isset($databaseRow['lastmodifiedat']) ? self::parseDateTimeString($databaseRow['lastmodifiedat']) : null, + isset($databaseRow['originallastmodifiedat']) ? self::parseDateTimeString($databaseRow['originallastmodifiedat']) : null, ); } + + private static function parseDateTimeString(string $string): \DateTimeImmutable + { + $result = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $string); + if ($result === false) { + throw new \RuntimeException(sprintf('Failed to parse "%s" into a valid DateTime', $string), 1678902055); + } + return $result; + } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index 402ed968b24..1f906303eda 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -77,10 +77,10 @@ public function mapNodeRowToNode( $this->nodeTypeManager->getNodeType($nodeRow['nodetypename']), $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['createdat']), - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originalcreatedat']), - isset($nodeRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originallastmodifiedat']) : null, + self::parseDateTimeString($nodeRow['createdat']), + self::parseDateTimeString($nodeRow['originalcreatedat']), + isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, ); } @@ -310,4 +310,13 @@ public function mapNodeRowsToNodeAggregates( ); } } + + private static function parseDateTimeString(string $string): \DateTimeImmutable + { + $result = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $string); + if ($result === false) { + throw new \RuntimeException(sprintf('Failed to parse "%s" into a valid DateTime', $string), 1678902055); + } + return $result; + } } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 5132f21159f..81268b4651a 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -88,10 +88,10 @@ public function mapNodeRowToNode( $this->propertyConverter ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['createdat']), - \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originalcreatedat']), - isset($nodeRow['lastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $nodeRow['originallastmodifiedat']) : null, + self::parseDateTimeString($nodeRow['createdat']), + self::parseDateTimeString($nodeRow['originalcreatedat']), + isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, ); return $result; @@ -344,4 +344,13 @@ public function mapNodeRowsToNodeAggregates(array $nodeRows, VisibilityConstrain ); } } + + private static function parseDateTimeString(string $string): \DateTimeImmutable + { + $result = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $string); + if ($result === false) { + throw new \RuntimeException(sprintf('Failed to parse "%s" into a valid DateTime', $string), 1678902055); + } + return $result; + } } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index bc68db3d8c8..7e8d5ba2357 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -40,6 +40,7 @@ use Neos\EventStore\Model\EventStore\SetupResult; use Neos\EventStore\Model\EventStream\VirtualStreamName; use Neos\EventStore\ProvidesSetupInterface; +use Psr\Clock\ClockInterface; /** * Main Entry Point to the system. Encapsulates the full event-sourced Content Repository. @@ -65,7 +66,8 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly InterDimensionalVariationGraph $variationGraph, private readonly ContentDimensionSourceInterface $contentDimensionSource, - private readonly UserIdProviderInterface $userIdProvider + private readonly UserIdProviderInterface $userIdProvider, + private readonly ClockInterface $clock, ) { } @@ -87,7 +89,7 @@ public function handle(CommandInterface $command): CommandResult // TODO meaningful exception message $initiatingUserId = $this->userIdProvider->getUserId(); - $initiatingTimestamp = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM); + $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. // This is done in order to keep information about the _original_ metadata when an @@ -128,6 +130,22 @@ public function withInitiatingUserId(UserId $userId): self $this->variationGraph, $this->contentDimensionSource, new StaticUserIdProvider($userId), + $this->clock, + ); + } + + public function withClock(ClockInterface $clock): self + { + return new self( + $this->commandBus, + $this->eventStore, + $this->projections, + $this->eventPersister, + $this->nodeTypeManager, + $this->variationGraph, + $this->contentDimensionSource, + $this->userIdProvider, + $clock, ); } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 6be65366d85..0324e0fd28a 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -14,6 +14,7 @@ namespace Neos\ContentRepository\Core\Factory; +use DateTimeImmutable; use Neos\ContentRepository\Core\CommandHandler\CommandBus; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; @@ -33,6 +34,7 @@ use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; use Neos\EventStore\EventStoreInterface; +use Psr\Clock\ClockInterface; use Symfony\Component\Serializer\Serializer; /** @@ -97,7 +99,12 @@ public function build(): ContentRepository $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, - $this->userIdProvider + $this->userIdProvider, + new class implements ClockInterface { + public function now(): DateTimeImmutable { + return new DateTimeImmutable(); + } + } ); } return $this->contentRepository; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php index f6b8ca9e5a2..fa679c8d412 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php @@ -30,34 +30,28 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; +use DateTimeImmutable; use GuzzleHttp\Psr7\Uri; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\ContentHypergraph; use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto\TraceEntryType; use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RedisInterleavingLogger; -use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\HypergraphProjection; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; -use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\ContentGraph\Subtrees; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; -use Neos\ContentRepository\Core\Tests\Behavior\Fixtures\DayOfWeek; -use Neos\ContentRepository\Security\Service\AuthorizationService; use Neos\ContentRepository\Core\Service\ContentStreamPruner; use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\ContentRepositoryRegistry\Factory\ProjectionCatchUpTrigger\CatchUpTriggerWithSynchronousOption; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Features\ContentStreamForking; @@ -77,13 +71,18 @@ use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\ContentRepositoryInternals; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\ContentRepositoryInternalsFactory; use Neos\ContentRepository\Core\Tests\Behavior\Features\Helper\ContentGraphs; +use Neos\ContentRepository\Core\Tests\Behavior\Fixtures\DayOfWeek; use Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress; +use Neos\ContentRepository\Security\Service\AuthorizationService; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepositoryRegistry\Factory\ProjectionCatchUpTrigger\CatchUpTriggerWithSynchronousOption; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\CheckpointException; use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Neos\FrontendRouting\NodeAddress; use PHPUnit\Framework\Assert; +use Psr\Clock\ClockInterface; /** * Features context @@ -473,6 +472,20 @@ public function theGraphProjectionIsFullyUpToDate() $this->lastCommandOrEventResult = null; } + /** + * @When the current date and time is :timestamp + */ + public function theCurrentDateAndTimeIs(string $timestamp): void + { + $now = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $timestamp); + $this->contentRepository = $this->contentRepository->withClock(new class ($now) implements ClockInterface { + public function __construct(private readonly DateTimeImmutable $now) {} + public function now(): DateTimeImmutable { + return $this->now; + } + }); + } + /** * @Then /^workspace "([^"]*)" points to another content stream than workspace "([^"]*)"$/ */ diff --git a/Neos.ContentRepository.Core/composer.json b/Neos.ContentRepository.Core/composer.json index 2bfbcb862b1..0aef0d44ce8 100644 --- a/Neos.ContentRepository.Core/composer.json +++ b/Neos.ContentRepository.Core/composer.json @@ -13,7 +13,8 @@ "neos/utility-objecthandling": "*", "neos/utility-arrays": "*", "doctrine/dbal": "^2.6", - "symfony/serializer": "*" + "symfony/serializer": "*", + "psr/clock": "^1" }, "require-dev": { "roave/security-advisories": "dev-latest", From 7942d9236b28f3832107a492a9ed7384e471cbf4 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 17 Mar 2023 10:32:45 +0100 Subject: [PATCH 03/12] WIP --- .../Features/Bootstrap/FeatureContext.php | 1 + .../Behavior/Bootstrap/FeatureContext.php | 1 + .../Behavior/Features/Timestamps.feature | 101 ++++++++++++++++++ .../Classes/ContentRepository.php | 2 +- .../Bootstrap/CurrentDateTimeTrait.php | 36 +++++++ .../Features/Bootstrap/EventSourcedTrait.php | 38 ++++--- .../Behavior/Bootstrap/FeatureContext.php | 1 + .../EventStore/DoctrineEventStoreFactory.php | 9 +- .../Features/Bootstrap/FeatureContext.php | 1 + 9 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature create mode 100644 Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php index af185fb285e..00d54d5038c 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -31,6 +31,7 @@ require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentSubgraphTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php'); +require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php'); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php index b317fc65ef9..9eb305c5729 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php @@ -28,6 +28,7 @@ require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentSubgraphTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php'); +require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php'); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature new file mode 100644 index 00000000000..e620c8da206 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature @@ -0,0 +1,101 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: TODO + + Background: + Given I have the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, ch | ch->de->mul, en->mul | + And I have the following NodeTypes configuration: + """ + 'Neos.ContentRepository:Root': [] + 'Neos.ContentRepository.Testing:AbstractPage': + abstract: true + properties: + text: + type: string + refs: + type: references + properties: + foo: + type: string + ref: + type: reference + properties: + foo: + type: string + 'Neos.ContentRepository.Testing:SomeMixin': + abstract: true + 'Neos.ContentRepository.Testing:Homepage': + superTypes: + 'Neos.ContentRepository.Testing:AbstractPage': true + childNodes: + terms: + type: 'Neos.ContentRepository.Testing:Terms' + contact: + type: 'Neos.ContentRepository.Testing:Contact' + + 'Neos.ContentRepository.Testing:Terms': + superTypes: + 'Neos.ContentRepository.Testing:AbstractPage': true + properties: + text: + defaultValue: 'Terms default' + 'Neos.ContentRepository.Testing:Contact': + superTypes: + 'Neos.ContentRepository.Testing:AbstractPage': true + 'Neos.ContentRepository.Testing:SomeMixin': true + properties: + text: + defaultValue: 'Contact default' + 'Neos.ContentRepository.Testing:Page': + superTypes: + 'Neos.ContentRepository.Testing:AbstractPage': true + 'Neos.ContentRepository.Testing:SpecialPage': + superTypes: + 'Neos.ContentRepository.Testing:AbstractPage': true + """ + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-live" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review" | + | baseWorkspaceName | "live" | + | newContentStreamId | "cs-review" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "review" | + | newContentStreamId | "cs-user" | + | workspaceOwner | "some-user" | + And the graph projection is fully up to date + And I am in content stream "cs-user" 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 graph projection is fully up to date + And the current date and time is "2023-03-16T12:00:00+01:00" + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} | + | a | a | Neos.ContentRepository.Testing:Page | home | {"text": "a"} | {} | + | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | + + Scenario: TODO + And the current date and time is "2023-03-16T13:00:00+01:00" + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | propertyValues | {"text": "Changed"} | + When I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned + And the graph projection is fully up to date + And I wait for 5 seconds + And the current date and time is "2023-03-16T14:00:00+01:00" + When the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And the graph projection is fully up to date diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 7e8d5ba2357..32b150ed118 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -138,7 +138,7 @@ public function withClock(ClockInterface $clock): self { return new self( $this->commandBus, - $this->eventStore, + $this->eventStore->withClock($clock), $this->projections, $this->eventPersister, $this->nodeTypeManager, diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php new file mode 100644 index 00000000000..2d7e13aa558 --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php @@ -0,0 +1,36 @@ +currentDateAndTime = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $timestamp); + } + + public function getCurrentDateAndTime(): ?DateTimeImmutable + { + return $this->currentDateAndTime; + } +} diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php index fa679c8d412..2e71c4ff56a 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php @@ -91,6 +91,7 @@ trait EventSourcedTrait { use CurrentSubgraphTrait; use CurrentUserTrait; + use CurrentDateTimeTrait; use NodeTraversalTrait; use ProjectedNodeAggregateTrait; use ProjectedNodeTrait; @@ -140,7 +141,20 @@ protected function getContentRepositoryRegistry(): ContentRepositoryRegistry protected function getContentRepository(): ContentRepository { $currentUserId = $this->getCurrentUserId(); - return $currentUserId === null ? $this->contentRepository : $this->contentRepository->withInitiatingUserId($currentUserId); + $currentDateAndTime = $this->getCurrentDateAndTime(); + $contentRepository = $this->contentRepository; + if ($currentUserId !== null) { + $contentRepository = $contentRepository->withInitiatingUserId($currentUserId); + } + if ($currentDateAndTime !== null) { + $contentRepository = $contentRepository->withClock(new class ($currentDateAndTime) implements ClockInterface { + public function __construct(private readonly DateTimeImmutable $now) {} + public function now(): DateTimeImmutable { + return $this->now; + } + }); + } + return $contentRepository; } /** @@ -460,6 +474,14 @@ protected function deserializeProperties(array $properties): PropertyValuesToWri return PropertyValuesToWrite::fromArray($properties); } + /** + * @When I wait for :seconds seconds + */ + public function iWait(int $seconds): void + { + sleep($seconds); + } + /** * @When /^the graph projection is fully up to date$/ */ @@ -472,20 +494,6 @@ public function theGraphProjectionIsFullyUpToDate() $this->lastCommandOrEventResult = null; } - /** - * @When the current date and time is :timestamp - */ - public function theCurrentDateAndTimeIs(string $timestamp): void - { - $now = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $timestamp); - $this->contentRepository = $this->contentRepository->withClock(new class ($now) implements ClockInterface { - public function __construct(private readonly DateTimeImmutable $now) {} - public function now(): DateTimeImmutable { - return $this->now; - } - }); - } - /** * @Then /^workspace "([^"]*)" points to another content stream than workspace "([^"]*)"$/ */ diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index ca59725606a..daa0517e698 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -5,6 +5,7 @@ require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeOperationsTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentSubgraphTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php'); +require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php'); require_once(__DIR__ . '/../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php'); diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php index b67f8326ee1..cd8dedfd325 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php @@ -7,6 +7,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\EventStore\DoctrineAdapter\DoctrineEventStore; use Neos\EventStore\EventStoreInterface; +use Psr\Clock\ClockInterface; class DoctrineEventStoreFactory implements EventStoreFactoryInterface { @@ -18,9 +19,15 @@ public function __construct( public function build(ContentRepositoryId $contentRepositoryIdentifier, array $contentRepositorySettings, array $eventStorePreset): EventStoreInterface { + $clock = new class implements ClockInterface { + public function now(): \DateTimeImmutable { + return new \DateTimeImmutable(); + } + }; return new DoctrineEventStore( $this->connection, - self::databaseTableName($contentRepositoryIdentifier) + self::databaseTableName($contentRepositoryIdentifier), + $clock ); } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index e39ab246ce8..4370d3f017b 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -43,6 +43,7 @@ require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentSubgraphTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php'); +require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php'); require_once(__DIR__ . '/../../../../../Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php'); From 1a856d7450b58c7d8be12b86f0c96bf75e4616fd Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 17 Mar 2023 18:28:26 +0100 Subject: [PATCH 04/12] WIP --- .../src/Domain/Repository/NodeFactory.php | 11 +++-- .../src/Domain/Repository/NodeFactory.php | 12 +++-- .../{ => NodeTraversal}/Timestamps.feature | 38 ++++++++++----- .../Classes/ContentRepository.php | 30 ------------ .../Factory/ContentRepositoryFactory.php | 13 ++--- .../Classes/Projection/ContentGraph/Node.php | 5 +- .../Projection/ContentGraph/Timestamps.php | 48 +++++++++++++++++++ .../Bootstrap/CurrentDateTimeTrait.php | 11 +---- .../Features/Bootstrap/CurrentUserTrait.php | 10 +--- .../Features/Bootstrap/EventSourcedTrait.php | 42 +++++----------- .../Features/ContentStreamForking.php | 2 - .../Bootstrap/Features/NodeCopying.php | 2 - .../Bootstrap/Features/NodeCreation.php | 2 - .../Bootstrap/Features/NodeDisabling.php | 2 - .../Bootstrap/Features/NodeModification.php | 2 - .../Features/Bootstrap/Features/NodeMove.php | 2 - .../Bootstrap/Features/NodeReferencing.php | 2 - .../Bootstrap/Features/NodeRemoval.php | 2 - .../Bootstrap/Features/NodeRenaming.php | 2 - .../Bootstrap/Features/NodeTypeChange.php | 2 - .../Bootstrap/Features/NodeVariation.php | 2 - .../Bootstrap/Features/WorkspaceCreation.php | 1 - .../Features/WorkspaceDiscarding.php | 1 - .../Features/WorkspacePublishing.php | 1 - .../Features/Bootstrap/Helpers/FakeClock.php | 22 +++++++++ .../Bootstrap/Helpers/FakeClockFactory.php | 16 +++++++ .../Bootstrap/Helpers/FakeUserIdProvider.php | 22 +++++++++ .../Helpers/FakeUserIdProviderFactory.php | 36 ++++++++++++++ .../Features/Bootstrap/NodeTraversalTrait.php | 24 +++++++++- .../Classes/ContentRepositoryRegistry.php | 17 ++++++- .../Factory/Clock/ClockFactoryInterface.php | 10 ++++ .../Classes/Factory/Clock/SystemClock.php | 14 ++++++ .../Factory/Clock/SystemClockFactory.php | 13 +++++ .../Configuration/Settings.yaml | 3 ++ Neos.ContentRepositoryRegistry/composer.json | 3 +- .../Functional/Fusion/NodeHelperTest.php | 6 +-- 36 files changed, 288 insertions(+), 143 deletions(-) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/{ => NodeTraversal}/Timestamps.feature (67%) create mode 100644 Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php create mode 100644 Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeClock.php create mode 100644 Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeClockFactory.php create mode 100644 Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeUserIdProvider.php create mode 100644 Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeUserIdProviderFactory.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Factory/Clock/SystemClock.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Factory/Clock/SystemClockFactory.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index 1f906303eda..638957c6951 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -21,6 +21,7 @@ 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\Timestamps; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -77,10 +78,12 @@ public function mapNodeRowToNode( $this->nodeTypeManager->getNodeType($nodeRow['nodetypename']), $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, - self::parseDateTimeString($nodeRow['createdat']), - self::parseDateTimeString($nodeRow['originalcreatedat']), - isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + Timestamps::create( + self::parseDateTimeString($nodeRow['createdat']), + self::parseDateTimeString($nodeRow['originalcreatedat']), + isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + ), ); } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 81268b4651a..04a896c5379 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtrees; +use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -88,10 +89,13 @@ public function mapNodeRowToNode( $this->propertyConverter ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, - self::parseDateTimeString($nodeRow['createdat']), - self::parseDateTimeString($nodeRow['originalcreatedat']), - isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + Timestamps::create( + // TODO replace with $nodeRow['createdat'] and $nodeRow['originalcreatedat'] once projection has implemented support + self::parseDateTimeString('2023-03-17 12:00:00'), + self::parseDateTimeString('2023-03-17 12:00:00'), + isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, + isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + ), ); return $result; diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature similarity index 67% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index e620c8da206..2842bbe4d53 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -1,4 +1,5 @@ @contentrepository @adapters=DoctrineDBAL + # TODO implement for Postgres Feature: TODO Background: @@ -79,23 +80,38 @@ Feature: TODO And the graph projection is fully up to date And the current date and time is "2023-03-16T12:00:00+01:00" And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds | - | home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} | - | a | a | Neos.ContentRepository.Testing:Page | home | {"text": "a"} | {} | - | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | + | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} | + | a | a | Neos.ContentRepository.Testing:Page | home | {"text": "a"} | {} | + | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | Scenario: TODO And the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeProperties is executed with payload: - | Key | Value | - | contentStreamId | "cs-user" | - | nodeAggregateId | "a" | - | propertyValues | {"text": "Changed"} | + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | propertyValues | {"text": "Changed"} | When I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned And the graph projection is fully up to date - And I wait for 5 seconds And the current date and time is "2023-03-16T14:00:00+01:00" When the command PublishWorkspace is executed with payload: - | Key | Value | - | workspaceName | "user-test" | + | Key | Value | + | workspaceName | "user-test" | And the graph projection is fully up to date + + And I am in content stream "cs-user" + Then I expect the node "a" to have the following timestamps: + | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + And I expect the node "b" to have the following timestamps: + | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + And I am in content stream "cs-review" + Then I expect the node "a" to have the following timestamps: + | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | + And I expect the node "b" to have the following timestamps: + | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | | | diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 32b150ed118..e1ab60571a0 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -119,36 +119,6 @@ public function handle(CommandInterface $command): CommandResult return $this->eventPersister->publishEvents($eventsToPublish); } - public function withInitiatingUserId(UserId $userId): self - { - return new self( - $this->commandBus, - $this->eventStore, - $this->projections, - $this->eventPersister, - $this->nodeTypeManager, - $this->variationGraph, - $this->contentDimensionSource, - new StaticUserIdProvider($userId), - $this->clock, - ); - } - - public function withClock(ClockInterface $clock): self - { - return new self( - $this->commandBus, - $this->eventStore->withClock($clock), - $this->projections, - $this->eventPersister, - $this->nodeTypeManager, - $this->variationGraph, - $this->contentDimensionSource, - $this->userIdProvider, - $clock, - ); - } - /** * @template T of ProjectionStateInterface * @param class-string $projectionStateClassName diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 0324e0fd28a..cd2d57ff52f 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Factory; -use DateTimeImmutable; use Neos\ContentRepository\Core\CommandHandler\CommandBus; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; @@ -28,10 +27,9 @@ use Neos\ContentRepository\Core\Feature\NodeDuplication\NodeDuplicationCommandHandler; use Neos\ContentRepository\Core\Feature\WorkspaceCommandHandler; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface; use Neos\ContentRepository\Core\Projection\Projections; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; @@ -56,6 +54,7 @@ public function __construct( ProjectionsFactory $projectionsFactory, private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, private readonly UserIdProviderInterface $userIdProvider, + private readonly ClockInterface $clock, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); $interDimensionalVariationGraph = new InterDimensionalVariationGraph( @@ -65,7 +64,7 @@ public function __construct( $this->projectionFactoryDependencies = new ProjectionFactoryDependencies( $contentRepositoryId, - $eventStore, + $eventStore->withClock($this->clock), new EventNormalizer(), $nodeTypeManager, $contentDimensionSource, @@ -100,11 +99,7 @@ public function build(): ContentRepository $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, $this->userIdProvider, - new class implements ClockInterface { - public function now(): DateTimeImmutable { - return new DateTimeImmutable(); - } - } + $this->clock, ); } return $this->contentRepository; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index b975204c17d..09e22a5b48a 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -68,10 +68,7 @@ public function __construct( */ public readonly PropertyCollectionInterface $properties, public readonly ?NodeName $nodeName, - public readonly \DateTimeImmutable $createdAt, - public readonly \DateTimeImmutable $originalCreatedAt, - public readonly ?\DateTimeImmutable $lastModifiedAt, - public readonly ?\DateTimeImmutable $originalLastModifiedAt, + public readonly Timestamps $timestamps, ) { } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php new file mode 100644 index 00000000000..95fcf5381aa --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -0,0 +1,48 @@ +currentDateAndTime = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $timestamp); - } - - public function getCurrentDateAndTime(): ?DateTimeImmutable - { - return $this->currentDateAndTime; + FakeClock::setNow(\DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $timestamp)); } } diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php index 2aed6121429..ef28a565751 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php @@ -13,25 +13,19 @@ */ use Neos\ContentRepository\Core\SharedModel\User\UserId; +use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\FakeUserIdProvider; /** * The feature trait to test projected nodes */ trait CurrentUserTrait { - protected ?UserId $currentUserId = null; - /** * @Given /^I am user identified by "([^"]*)"$/ * @param string $userId */ public function iAmUserIdentifiedBy(string $userId): void { - $this->currentUserId = UserId::fromString($userId); - } - - public function getCurrentUserId(): ?UserId - { - return $this->currentUserId; + FakeUserIdProvider::setUserId(UserId::fromString($userId)); } } diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php index 2e71c4ff56a..cfd771fba87 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php @@ -30,7 +30,6 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; -use DateTimeImmutable; use GuzzleHttp\Psr7\Uri; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\ContentHypergraph; use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto\TraceEntryType; @@ -70,6 +69,9 @@ use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Features\WorkspacePublishing; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\ContentRepositoryInternals; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\ContentRepositoryInternalsFactory; +use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\FakeClockFactory; +use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\FakeUserIdProviderFactory; +use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\MutableClockFactory; use Neos\ContentRepository\Core\Tests\Behavior\Features\Helper\ContentGraphs; use Neos\ContentRepository\Core\Tests\Behavior\Fixtures\DayOfWeek; use Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress; @@ -82,7 +84,6 @@ use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Neos\FrontendRouting\NodeAddress; use PHPUnit\Framework\Assert; -use Psr\Clock\ClockInterface; /** * Features context @@ -140,21 +141,7 @@ protected function getContentRepositoryRegistry(): ContentRepositoryRegistry protected function getContentRepository(): ContentRepository { - $currentUserId = $this->getCurrentUserId(); - $currentDateAndTime = $this->getCurrentDateAndTime(); - $contentRepository = $this->contentRepository; - if ($currentUserId !== null) { - $contentRepository = $contentRepository->withInitiatingUserId($currentUserId); - } - if ($currentDateAndTime !== null) { - $contentRepository = $contentRepository->withClock(new class ($currentDateAndTime) implements ClockInterface { - public function __construct(private readonly DateTimeImmutable $now) {} - public function now(): DateTimeImmutable { - return $this->now; - } - }); - } - return $contentRepository; + return $this->contentRepository; } /** @@ -255,14 +242,17 @@ private function initCleanContentRepository(array $adapterKeys): void // // This is to make the testcases more stable and deterministic. We can remove this workaround // once the Postgres adapter is fully ready. - unset($registrySettings['presets']['default']['projections']['Neos.ContentGraph.PostgreSQLAdapter:Hypergraph']); + unset($registrySettings['presets'][$this->contentRepositoryId->value]['projections']['Neos.ContentGraph.PostgreSQLAdapter:Hypergraph']); } + $registrySettings['presets'][$this->contentRepositoryId->value]['userIdProvider']['factoryObjectName'] = FakeUserIdProviderFactory::class; + $registrySettings['presets'][$this->contentRepositoryId->value]['clock']['factoryObjectName'] = FakeClockFactory::class; $this->contentRepositoryRegistry = new ContentRepositoryRegistry( $registrySettings, $this->getObjectManager() ); + $this->contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); // Big performance optimization: only run the setup once - DRAMATICALLY reduces test time if ($this->alwaysRunContentRepositorySetup || !self::$wasContentRepositorySetupCalled) { @@ -386,7 +376,7 @@ public function beforeEventSourcedScenarioDispatcher(BeforeScenarioScope $scope) // if we end up here without exception, we know the projection states were properly reset. $projectionsWereReset = true; - } catch(CheckpointException $checkpointException) { + } catch (CheckpointException $checkpointException) { // TODO: HACK: UGLY CODE!!! if ($checkpointException->getCode() === 1652279016 && $retryCount < 20) { // we wait for 10 seconds max. // another process is in the critical section; a.k.a. @@ -474,14 +464,6 @@ protected function deserializeProperties(array $properties): PropertyValuesToWri return PropertyValuesToWrite::fromArray($properties); } - /** - * @When I wait for :seconds seconds - */ - public function iWait(int $seconds): void - { - sleep($seconds); - } - /** * @When /^the graph projection is fully up to date$/ */ @@ -542,8 +524,7 @@ public function theSubtreeForNodeAggregateWithNodeTypesAndLevelsDeepShouldBe( string $serializedNodeTypeConstraints, int $maximumLevels, TableNode $table - ): void - { + ): void { $nodeAggregateId = NodeAggregateId::fromString($serializedNodeAggregateId); $nodeTypeConstraints = NodeTypeConstraints::fromFilterString($serializedNodeTypeConstraints); foreach ($this->getActiveContentGraphs() as $adapterName => $contentGraph) { @@ -569,7 +550,8 @@ public function theSubtreeForNodeAggregateWithNodeTypesAndLevelsDeepShouldBe( Assert::assertSame($expectedLevel, $actualLevel, 'Level does not match in index ' . $i . ', expected: ' . $expectedLevel . ', actual: ' . $actualLevel . ' (adapter: ' . $adapterName . ')'); $expectedNodeAggregateId = NodeAggregateId::fromString($expectedRow['nodeAggregateId']); $actualNodeAggregateId = $flattenedSubtree[$i]->node->nodeAggregateId; - Assert::assertTrue($expectedNodeAggregateId->equals($actualNodeAggregateId), 'NodeAggregateId does not match in index ' . $i . ', expected: "' . $expectedNodeAggregateId . '", actual: "' . $actualNodeAggregateId . '" (adapter: ' . $adapterName . ')'); + Assert::assertTrue($expectedNodeAggregateId->equals($actualNodeAggregateId), + 'NodeAggregateId does not match in index ' . $i . ', expected: "' . $expectedNodeAggregateId . '", actual: "' . $actualNodeAggregateId . '" (adapter: ' . $adapterName . ')'); } } } diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/ContentStreamForking.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/ContentStreamForking.php index fe7f8b57b96..058e49353f9 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/ContentStreamForking.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/ContentStreamForking.php @@ -28,8 +28,6 @@ abstract protected function getContentRepository(): ContentRepository; abstract protected function getCurrentContentStreamId(): ?ContentStreamId; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; /** diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCopying.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCopying.php index a654fb864f8..f00ec6f6a95 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCopying.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCopying.php @@ -38,8 +38,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function getAvailableContentGraphs(): ContentGraphs; abstract protected function getCurrentNodes(): ?NodesByAdapter; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php index 70cc7a7fc05..4a62a577c47 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php @@ -43,8 +43,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function deserializeProperties(array $properties): PropertyValuesToWrite; abstract protected function readPayloadTable(TableNode $payloadTable): array; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeDisabling.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeDisabling.php index d7983f55e86..c2b26ac050e 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeDisabling.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeDisabling.php @@ -36,8 +36,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeModification.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeModification.php index 18378361083..6774cf94604 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeModification.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeModification.php @@ -37,8 +37,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeMove.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeMove.php index c446bd21857..be9736c32ac 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeMove.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeMove.php @@ -35,8 +35,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeReferencing.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeReferencing.php index f66bb5e48e8..c2e13559719 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeReferencing.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeReferencing.php @@ -41,8 +41,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function deserializeProperties(array $properties): PropertyValuesToWrite; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRemoval.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRemoval.php index 604cdce11d4..c6a62f4265b 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRemoval.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRemoval.php @@ -35,8 +35,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRenaming.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRenaming.php index 63dcd2e5cf0..a575ddaa809 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeRenaming.php @@ -31,8 +31,6 @@ abstract protected function getCurrentContentStreamId(): ?ContentStreamId; abstract protected function getCurrentDimensionSpacePoint(): ?DimensionSpacePoint; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeTypeChange.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeTypeChange.php index d083ffffe12..d2c59852c5b 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeTypeChange.php @@ -32,8 +32,6 @@ abstract protected function getContentRepository(): ContentRepository; abstract protected function getCurrentContentStreamId(): ?ContentStreamId; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; /** diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeVariation.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeVariation.php index 2f01769952a..436bd56ab93 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeVariation.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeVariation.php @@ -32,8 +32,6 @@ abstract protected function getContentRepository(): ContentRepository; abstract protected function getCurrentContentStreamId(): ?ContentStreamId; - abstract protected function getCurrentUserId(): ?UserId; - abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php index b5fd8715288..b65fb530308 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php @@ -32,7 +32,6 @@ trait WorkspaceCreation { abstract protected function getContentRepository(): ContentRepository; - abstract protected function getCurrentUserId(): ?UserId; abstract protected function readPayloadTable(TableNode $payloadTable): array; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php index 3dabeeb0959..9b26468c1d2 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php @@ -27,7 +27,6 @@ trait WorkspaceDiscarding { abstract protected function getContentRepository(): ContentRepository; - abstract protected function getCurrentUserId(): ?UserId; abstract protected function readPayloadTable(TableNode $payloadTable): array; diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php index 8e3c0d5090a..249a6ac52a8 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php @@ -27,7 +27,6 @@ trait WorkspacePublishing { abstract protected function getContentRepository(): ContentRepository; - abstract protected function getCurrentUserId(): ?UserId; abstract protected function readPayloadTable(TableNode $payloadTable): array; /** diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeClock.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeClock.php new file mode 100644 index 00000000000..563ce4c553d --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Helpers/FakeClock.php @@ -0,0 +1,22 @@ + $timestamp === '' ? null : \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $timestamp), $expectedTimestampsTable->getHash()[0]); + /** @var ContentSubgraphInterface $subgraph */ + foreach ($this->getCurrentSubgraphs() as $subgraph) { + $node = $subgraph->findNodeById($nodeAggregateId); + if ($node === null) { + Assert::fail(sprintf('Failed to find node with aggregate id "%s"', $nodeAggregateId->value)); + } + $actualTimestamps = [ + 'createdAt' => $node->timestamps->createdAt, + 'originalCreatedAt' => $node->timestamps->originalCreatedAt, + 'lastModifiedAt' => $node->timestamps->lastModifiedAt, + 'originalLastModifiedAt' => $node->timestamps->originalLastModifiedAt, + ]; + Assert::assertEquals($expectedTimestamps, $actualTimestamps); + } + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 89755c092b9..67d09e58052 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -15,8 +15,10 @@ use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface; use Neos\ContentRepository\Core\Projection\ProjectionFactoryInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFound; use Neos\ContentRepositoryRegistry\Exception\InvalidConfigurationException; +use Neos\ContentRepositoryRegistry\Factory\Clock\ClockFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\ContentDimensionSource\ContentDimensionSourceFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\EventStore\EventStoreFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\NodeTypeManager\NodeTypeManagerFactoryInterface; @@ -28,6 +30,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Utility\PositionalArraySorter; +use Psr\Clock\ClockInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -98,11 +101,10 @@ public function getService(ContentRepositoryId $contentRepositoryIdentifier, Con return $this->contentRepositoryServiceInstances[$contentRepositoryIdentifier->value][get_class($contentRepositoryServiceFactory)]; } - /** * @throws ContentRepositoryNotFound | InvalidConfigurationException */ - private function getFactory(ContentRepositoryId $contentRepositoryIdentifier): ContentRepositoryFactory + public function getFactory(ContentRepositoryId $contentRepositoryIdentifier): ContentRepositoryFactory { // This cache is CRUCIAL, because it ensures that the same CR always deals with the same objects internally, even if multiple services // are called on the same CR. @@ -135,6 +137,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryIdentifier): $this->buildProjectionsFactory($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset), $this->buildProjectionCatchUpTrigger($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset), $this->buildUserIdProvider($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset), + $this->buildClock($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset), ); } catch (\Exception $exception) { throw InvalidConfigurationException::fromException($contentRepositoryIdentifier, $exception); @@ -239,4 +242,14 @@ private function buildUserIdProvider(ContentRepositoryId $contentRepositoryIdent } return $userIdProviderFactory->build($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset); } + + private function buildClock(ContentRepositoryId $contentRepositoryIdentifier, array $contentRepositorySettings, array $contentRepositoryPreset): ClockInterface + { + assert(isset($contentRepositoryPreset['clock']['factoryObjectName']), InvalidConfigurationException::fromMessage('Content repository preset "%s" does not have clock.factoryObjectName configured.', $contentRepositorySettings['preset'])); + $clockFactory = $this->objectManager->get($contentRepositoryPreset['clock']['factoryObjectName']); + if (!$clockFactory instanceof ClockFactoryInterface) { + throw new \RuntimeException(sprintf('clock.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryIdentifier->value, ClockFactoryInterface::class, get_debug_type($clockFactory))); + } + return $clockFactory->build(); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php new file mode 100644 index 00000000000..f09c0e67616 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php @@ -0,0 +1,10 @@ + Date: Wed, 29 Mar 2023 17:36:42 +0200 Subject: [PATCH 05/12] Rename remaining occurrences of *createdAt and *lastModifiedAt --- .../DoctrineDbalContentGraphProjection.php | 24 +++++------ .../DoctrineDbalContentGraphSchemaBuilder.php | 8 ++-- .../src/Domain/Projection/NodeRecord.php | 40 +++++++++---------- .../src/Domain/Repository/NodeFactory.php | 8 ++-- .../src/Domain/Repository/NodeFactory.php | 6 +-- .../Features/NodeTraversal/Timestamps.feature | 18 ++++----- .../Projection/ContentGraph/Timestamps.php | 10 ++--- .../Features/Bootstrap/NodeTraversalTrait.php | 8 ++-- 8 files changed, 61 insertions(+), 61 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 8035cb895a7..889e755452d 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -390,8 +390,8 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev h.childnodeanchor = n.relationanchorpoint SET h.name = :newName, - n.lastmodifiedat = :lastModifiedAt, - n.originallastmodifiedat = :originalLastModifiedAt + n.lastmodified = :lastModified, + n.originallastmodified = :originalLastModified WHERE n.nodeaggregateid = :nodeAggregateId @@ -400,11 +400,11 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev 'newName' => (string)$event->newNodeName, 'nodeAggregateId' => (string)$event->nodeAggregateId, 'contentStreamId' => (string)$event->contentStreamId, - 'lastModifiedAt' => $eventEnvelope->recordedAt, - 'originalLastModifiedAt' => self::initiatingDateTime($eventEnvelope), + 'lastModified' => $eventEnvelope->recordedAt, + 'originalLastModified' => self::initiatingDateTime($eventEnvelope), ], [ - 'lastModifiedAt' => Types::DATETIME_IMMUTABLE, - 'originalLastModifiedAt' => Types::DATETIME_IMMUTABLE, + 'lastModified' => Types::DATETIME_IMMUTABLE, + 'originalLastModified' => Types::DATETIME_IMMUTABLE, ]); }); } @@ -764,8 +764,8 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn $this->transactional(function () use ($event, $eventEnvelope) { $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties->merge($event->propertyValues); - $node->lastModifiedAt = $eventEnvelope->recordedAt; - $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); + $node->lastModified = $eventEnvelope->recordedAt; + $node->originalLastModified = self::initiatingDateTime($eventEnvelope); }); }); } @@ -798,8 +798,8 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn $event->contentStreamId, $nodeAnchorPoint, function (NodeRecord $node) use ($eventEnvelope) { - $node->lastModifiedAt = $eventEnvelope->recordedAt; - $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); + $node->lastModified = $eventEnvelope->recordedAt; + $node->originalLastModified = self::initiatingDateTime($eventEnvelope); } ); @@ -1013,8 +1013,8 @@ private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $ev $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; - $node->lastModifiedAt = $eventEnvelope->recordedAt; - $node->originalLastModifiedAt = self::initiatingDateTime($eventEnvelope); + $node->lastModified = $eventEnvelope->recordedAt; + $node->originalLastModified = self::initiatingDateTime($eventEnvelope); } ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 94774a77869..5368bc7fb33 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -50,14 +50,14 @@ private function createNodeTable(Schema $schema): void $table->addColumn('classification', Types::STRING) ->setLength(255) ->setNotnull(true); - $table->addColumn('createdat', Types::DATETIME_IMMUTABLE) + $table->addColumn('created', Types::DATETIME_IMMUTABLE) ->setNotnull(true); - $table->addColumn('originalcreatedat', Types::DATETIME_IMMUTABLE) + $table->addColumn('originalcreated', Types::DATETIME_IMMUTABLE) ->setNotnull(true); - $table->addColumn('lastmodifiedat', Types::DATETIME_IMMUTABLE) + $table->addColumn('lastmodified', Types::DATETIME_IMMUTABLE) ->setNotnull(false) ->setDefault(null); - $table->addColumn('originallastmodifiedat', Types::DATETIME_IMMUTABLE) + $table->addColumn('originallastmodified', Types::DATETIME_IMMUTABLE) ->setNotnull(false) ->setDefault(null); $table diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index 152df61585a..b1086d03210 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -40,10 +40,10 @@ public function __construct( public NodeAggregateClassification $classification, /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ public ?NodeName $nodeName, - public \DateTimeImmutable $createdAt, - public \DateTimeImmutable $originalCreatedAt, - public ?\DateTimeImmutable $lastModifiedAt, - public ?\DateTimeImmutable $originalLastModifiedAt, + public \DateTimeImmutable $created, + public \DateTimeImmutable $originalCreated, + public ?\DateTimeImmutable $lastModified, + public ?\DateTimeImmutable $originalLastModified, ) { } @@ -61,15 +61,15 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, 'classification' => $this->classification->value, - 'createdat' => $this->createdAt, - 'originalcreatedat' => $this->originalCreatedAt, - 'lastmodifiedat' => $this->lastModifiedAt, - 'originallastmodifiedat' => $this->originalLastModifiedAt, + 'created' => $this->created, + 'originalcreated' => $this->originalCreated, + 'lastmodified' => $this->lastModified, + 'originallastmodified' => $this->originalLastModified, ], [ - 'createdat' => Types::DATETIME_IMMUTABLE, - 'originalcreatedat' => Types::DATETIME_IMMUTABLE, - 'lastmodifiedat' => Types::DATETIME_IMMUTABLE, - 'originallastmodifiedat' => Types::DATETIME_IMMUTABLE, + 'created' => Types::DATETIME_IMMUTABLE, + 'originalcreated' => Types::DATETIME_IMMUTABLE, + 'lastmodified' => Types::DATETIME_IMMUTABLE, + 'originallastmodified' => Types::DATETIME_IMMUTABLE, ]); } @@ -88,15 +88,15 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, 'classification' => $this->classification->value, - 'lastmodifiedat' => $this->lastModifiedAt, - 'originallastmodifiedat' => $this->originalLastModifiedAt, + 'lastmodified' => $this->lastModified, + 'originallastmodified' => $this->originalLastModified, ], [ 'relationanchorpoint' => $this->relationAnchorPoint ], [ - 'lastmodifiedat' => Types::DATETIME_IMMUTABLE, - 'originallastmodifiedat' => Types::DATETIME_IMMUTABLE, + 'lastmodified' => Types::DATETIME_IMMUTABLE, + 'originallastmodified' => Types::DATETIME_IMMUTABLE, ] ); } @@ -128,10 +128,10 @@ public static function fromDatabaseRow(array $databaseRow): self NodeTypeName::fromString($databaseRow['nodetypename']), NodeAggregateClassification::from($databaseRow['classification']), isset($databaseRow['name']) ? NodeName::fromString($databaseRow['name']) : null, - self::parseDateTimeString($databaseRow['createdat']), - self::parseDateTimeString($databaseRow['originalcreatedat']), - isset($databaseRow['lastmodifiedat']) ? self::parseDateTimeString($databaseRow['lastmodifiedat']) : null, - isset($databaseRow['originallastmodifiedat']) ? self::parseDateTimeString($databaseRow['originallastmodifiedat']) : null, + self::parseDateTimeString($databaseRow['created']), + self::parseDateTimeString($databaseRow['originalcreated']), + isset($databaseRow['lastmodified']) ? self::parseDateTimeString($databaseRow['lastmodified']) : null, + isset($databaseRow['originallastmodified']) ? self::parseDateTimeString($databaseRow['originallastmodified']) : null, ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index 638957c6951..4a528a06c52 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -79,10 +79,10 @@ public function mapNodeRowToNode( $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, Timestamps::create( - self::parseDateTimeString($nodeRow['createdat']), - self::parseDateTimeString($nodeRow['originalcreatedat']), - isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + self::parseDateTimeString($nodeRow['created']), + self::parseDateTimeString($nodeRow['originalcreated']), + isset($nodeRow['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, + isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, ), ); } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index be66e8f8f1d..2f33aefbf95 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -90,11 +90,11 @@ public function mapNodeRowToNode( ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, Timestamps::create( - // TODO replace with $nodeRow['createdat'] and $nodeRow['originalcreatedat'] once projection has implemented support + // TODO replace with $nodeRow['created'] and $nodeRow['originalcreated'] once projection has implemented support self::parseDateTimeString('2023-03-17 12:00:00'), self::parseDateTimeString('2023-03-17 12:00:00'), - isset($nodeRow['lastmodifiedat']) ? self::parseDateTimeString($nodeRow['lastmodifiedat']) : null, - isset($nodeRow['originallastmodifiedat']) ? self::parseDateTimeString($nodeRow['originallastmodifiedat']) : null, + isset($nodeRow['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, + isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, ), ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index 2842bbe4d53..3a792ad8060 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -86,7 +86,7 @@ Feature: TODO | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | Scenario: TODO - And the current date and time is "2023-03-16T13:00:00+01:00" + When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeProperties is executed with payload: | Key | Value | | contentStreamId | "cs-user" | @@ -102,16 +102,16 @@ Feature: TODO And I am in content stream "cs-user" Then I expect the node "a" to have the following timestamps: - | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | And I expect the node "b" to have the following timestamps: - | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | And I am in content stream "cs-review" Then I expect the node "a" to have the following timestamps: - | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | - | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | And I expect the node "b" to have the following timestamps: - | createdAt | originalCreatedAt | lastModifiedAt | originalLastModifiedAt | - | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | | | + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | | | diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php index 778ff7fd99d..8e77b0ba82b 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -38,11 +38,11 @@ private function __construct( } public static function create( - DateTimeImmutable $createdAt, - DateTimeImmutable $originalCreatedAt, - ?DateTimeImmutable $lastModifiedAt, - ?DateTimeImmutable $originalLastModifiedAt + DateTimeImmutable $created, + DateTimeImmutable $originalCreated, + ?DateTimeImmutable $lastModified, + ?DateTimeImmutable $originalLastModified ): self { - return new self($createdAt, $originalCreatedAt, $lastModifiedAt, $originalLastModifiedAt); + return new self($created, $originalCreated, $lastModified, $originalLastModified); } } diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php index 48f98ce0ffc..26ab45ac7ad 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php @@ -408,10 +408,10 @@ public function iExpectTheNodeToHaveTheFollowingTimestamps(string $nodeIdSeriali Assert::fail(sprintf('Failed to find node with aggregate id "%s"', $nodeAggregateId->value)); } $actualTimestamps = [ - 'createdAt' => $node->timestamps->created, - 'originalCreatedAt' => $node->timestamps->originalCreated, - 'lastModifiedAt' => $node->timestamps->lastModified, - 'originalLastModifiedAt' => $node->timestamps->originalLastModified, + 'created' => $node->timestamps->created, + 'originalCreated' => $node->timestamps->originalCreated, + 'lastModified' => $node->timestamps->lastModified, + 'originalLastModified' => $node->timestamps->originalLastModified, ]; Assert::assertEquals($expectedTimestamps, $actualTimestamps); } From 185f4b673049e51385fb5bb27c02d9170cafc4a0 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Thu, 30 Mar 2023 15:15:03 +0200 Subject: [PATCH 06/12] Only update timestamps of affected DSPs --- .../DoctrineDbalContentGraphProjection.php | 86 ++++---- .../Projection/Feature/NodeDisabling.php | 13 +- .../Domain/Projection/Feature/NodeMove.php | 22 +- .../Repository/ProjectionContentGraph.php | 33 ++- .../Features/NodeTraversal/Timestamps.feature | 196 +++++++++++++++++- 5 files changed, 282 insertions(+), 68 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 889e755452d..84043f316ea 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; @@ -762,11 +761,29 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { - $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event, $eventEnvelope) { - $node->properties = $node->properties->merge($event->propertyValues); - $node->lastModified = $eventEnvelope->recordedAt; - $node->originalLastModified = self::initiatingDateTime($eventEnvelope); - }); + $anchorPoint = $this->projectionContentGraph + ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( + $event->getNodeAggregateId(), + $event->getOriginDimensionSpacePoint(), + $event->getContentStreamId() + ); + if (is_null($anchorPoint)) { + throw new \InvalidArgumentException( + 'Cannot update node with copy on write since no anchor point could be resolved for node ' + . $event->getNodeAggregateId() . ' in content stream ' + . $event->getContentStreamId(), + 1645303332 + ); + } + $this->updateNodeRecordWithCopyOnWrite( + $event->getContentStreamId(), + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->properties = $node->properties->merge($event->propertyValues); + $node->lastModified = $eventEnvelope->recordedAt; + $node->originalLastModified = self::initiatingDateTime($eventEnvelope); + } + ); }); } @@ -925,13 +942,13 @@ private function cascadeRestrictionRelations( */ private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( $event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints ); - // TODO update last modified values + $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, $event->affectedDimensionSpacePoints, $eventEnvelope); }); } @@ -1002,11 +1019,7 @@ protected function copyNodeToDimensionSpacePoint( private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( - $event->nodeAggregateId, - $event->contentStreamId - ); - + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $event->contentStreamId); foreach ($anchorPoints as $anchorPoint) { $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, @@ -1021,46 +1034,19 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { }); } - /** - * @throws \Doctrine\DBAL\DBALException - * @throws \Exception - */ - private function updateNodeWithCopyOnWrite(EventInterface $event, callable $operations): mixed + protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void { - if ( - method_exists($event, 'getNodeAggregateId') - && method_exists($event, 'getOriginDimensionSpacePoint') - && method_exists($event, 'getContentStreamId') - ) { - $anchorPoint = $this->projectionContentGraph - ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( - $event->getNodeAggregateId(), - $event->getOriginDimensionSpacePoint(), - $event->getContentStreamId() - ); - } else { - throw new \InvalidArgumentException( - 'Cannot update node with copy on write for events of type ' - . get_class($event) . ' since they provide no NodeAggregateId, ' - . 'OriginDimensionSpacePoint or ContentStreamId', - 1645303167 - ); - } - - if (is_null($anchorPoint)) { - throw new \InvalidArgumentException( - 'Cannot update node with copy on write since no anchor point could be resolved for node ' - . $event->getNodeAggregateId() . ' in content stream ' - . $event->getContentStreamId(), - 1645303332 + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAndDimensionSpacePointsAndContentStream($nodeAggregateId, $affectedDimensionSpacePoints, $contentStreamId); + foreach ($anchorPoints as $anchorPoint) { + $this->updateNodeRecordWithCopyOnWrite( + $contentStreamId, + $anchorPoint, + function (NodeRecord $node) use ($eventEnvelope) { + $node->lastModified = $eventEnvelope->recordedAt; + $node->originalLastModified = self::initiatingDateTime($eventEnvelope); + } ); } - - return $this->updateNodeRecordWithCopyOnWrite( - $event->getContentStreamId(), - $anchorPoint, - $operations - ); } private function updateNodeRecordWithCopyOnWrite( diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php index 0db7469dc92..28ec7a3a948 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php @@ -5,7 +5,12 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Types; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\EventStore\Model\EventEnvelope; /** @@ -17,12 +22,16 @@ trait NodeDisabling { abstract protected function getTableNamePrefix(): string; + abstract protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void; + /** * @throws \Throwable */ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { + + // TODO: still unsure why we need an "INSERT IGNORE" here; // normal "INSERT" can trigger a duplicate key constraint exception $this->getDatabaseConnection()->executeStatement( @@ -84,7 +93,7 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event, E 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY ] ); - // TODO update last modified values + $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, $event->affectedDimensionSpacePoints, $eventEnvelope); }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index 836e46b1ecf..de9a3398fe6 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -35,13 +35,18 @@ abstract protected function getProjectionContentGraph(): ProjectionContentGraph; abstract protected function getTableNamePrefix(): string; + abstract protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void; + /** * @param NodeAggregateWasMoved $event * @throws \Throwable */ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { + $this->transactional(function () use ($event, $eventEnvelope) { + + $dimensionSpacePoints = []; + foreach ($event->nodeMoveMappings as $moveNodeMapping) { // for each materialized node in the DB which we want to adjust, we have one MoveNodeMapping. /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveNodeMapping */ @@ -58,13 +63,16 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEn foreach ($moveNodeMapping->newLocations as $newLocation) { assert($newLocation instanceof CoverageNodeMoveMapping); + $dimensionSpacePoints[] = $newLocation->coveredDimensionSpacePoint; + $affectedDimensionSpacePoints = new DimensionSpacePointSet([ + $newLocation->coveredDimensionSpacePoint + ]); + // remove restriction relations by ancestors. We will reconnect them back after the move. $this->removeAllRestrictionRelationsInSubtreeImposedByAncestors( $event->contentStreamId, $event->nodeAggregateId, - new DimensionSpacePointSet([ - $newLocation->coveredDimensionSpacePoint - ]) + $affectedDimensionSpacePoints ); // do the move (depending on how the move target is specified) @@ -95,13 +103,11 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEn $event->contentStreamId, $newParentNodeAggregateId, $event->nodeAggregateId, - new DimensionSpacePointSet([ - $newLocation->coveredDimensionSpacePoint - ]) + $affectedDimensionSpacePoints ); } } - // TODO update last modified values + $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, DimensionSpacePointSet::fromArray($dimensionSpacePoints), $eventEnvelope); }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 33c02d07702..927daa1bdd8 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -177,9 +177,38 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea if (count($rows) === 1) { return NodeRelationAnchorPoint::fromString($rows[0]['relationanchorpoint']); - } else { - return null; } + return null; + } + + /** + * @return iterable + */ + public function getAnchorPointsForNodeAndDimensionSpacePointsAndContentStream( + NodeAggregateId $nodeAggregateId, + DimensionSpacePointSet $dimensionSpacePoints, + ContentStreamId $contentStreamId + ): iterable { + $rows = $this->getDatabaseConnection()->executeQuery( + 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNamePrefix . '_node n + INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + WHERE n.nodeaggregateid = :nodeAggregateId + AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes) + AND h.contentstreamid = :contentStreamId', + [ + 'nodeAggregateId' => (string)$nodeAggregateId, + 'dimensionSpacePointHashes' => $dimensionSpacePoints->getPointHashes(), + 'contentStreamId' => (string)$contentStreamId, + ], + [ + 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, + ] + )->fetchAllAssociative(); + + return array_map( + static fn(array $row) => NodeRelationAnchorPoint::fromString($row['relationanchorpoint']), + $rows + ); } /** diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index 3a792ad8060..4b671a434ee 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -1,9 +1,10 @@ @contentrepository @adapters=DoctrineDBAL # TODO implement for Postgres -Feature: TODO +Feature: Behavior of Node timestamp properties "created", "originalCreated", "lastModified" and "originalLastModified" Background: - Given I have the following content dimensions: + Given the current date and time is "2023-03-16T12:00:00+01:00" + And I have the following content dimensions: | Identifier | Values | Generalizations | | language | mul, de, en, ch | ch->de->mul, en->mul | And I have the following NodeTypes configuration: @@ -78,24 +79,194 @@ Feature: TODO | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | And the graph projection is fully up to date - And the current date and time is "2023-03-16T12:00:00+01:00" And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds | | home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} | | a | a | Neos.ContentRepository.Testing:Page | home | {"text": "a"} | {} | | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"ch"} | + And the graph projection is fully up to date + + Scenario: NodePropertiesWereSet events update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | originDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "a" | + | propertyValues | {"text": "Changed"} | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + Scenario: NodeAggregateNameWasChanged events update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command "ChangeNodeAggregateName" is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | newNodeName | "a-renamed" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + Scenario: NodeReferencesWereSet events update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command SetNodeReferences is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | sourceOriginDimensionSpacePoint | {"language": "ch"} | + | sourceNodeAggregateId | "a" | + | referenceName | "ref" | + | references | [{"target": "b"}] | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + And I expect the node "b" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + And I expect the node "b" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + Scenario: NodeAggregateWasEnabled and NodeAggregateWasDisabled events 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 | + | contentStreamId | "cs-user" | + | coveredDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + And VisibilityConstraints are set to "withoutRestrictions" + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + When the current date and time is "2023-03-16T14:00:00+01:00" + And the command EnableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | coveredDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + + Scenario: NodeAggregateTypeWasChanged events update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command ChangeNodeAggregateType was published with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | + | strategy | "happypath" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + Scenario: NodeAggregateWasMoved events update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | dimensionSpacePoint | {"language": "ch"} | + | relationDistributionStrategy | "gatherSpecializations" | + | nodeAggregateId | "a" | + | newParentNodeAggregateId | "b" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + And I expect the node "b" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + And I expect the node "b" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + - Scenario: TODO + Scenario: RootNodeAggregateDimensionsWereUpdated events don't update last modified timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command UpdateRootNodeAggregateDimensions is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + + Scenario: Original created and last modified timestamps when publishing nodes over multiple content streams When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeProperties is executed with payload: | Key | Value | | contentStreamId | "cs-user" | | nodeAggregateId | "a" | | propertyValues | {"text": "Changed"} | - When I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned + And I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned And the graph projection is fully up to date And the current date and time is "2023-03-16T14:00:00+01:00" - When the command PublishWorkspace is executed with payload: + And the command PublishWorkspace is executed with payload: | Key | Value | | workspaceName | "user-test" | And the graph projection is fully up to date @@ -115,3 +286,16 @@ Feature: TODO And I expect the node "b" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | | | + + When the current date and time is "2023-03-16T15:00:00+01:00" + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review" | + And the graph projection is fully up to date + And I am in content stream "cs-live" + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 15:00:00 | 2023-03-16 12:00:00 | 2023-03-16 15:00:00 | 2023-03-16 13:00:00 | + And I expect the node "b" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 15:00:00 | 2023-03-16 12:00:00 | | | From 744ae3d1a9b0e7e6c1118c1f9cca1a6deb3a6e61 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Thu, 30 Mar 2023 15:15:21 +0200 Subject: [PATCH 07/12] Add doc comments to `Timestamps` VO --- .../Projection/ContentGraph/Timestamps.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php index 8e77b0ba82b..c5ce2ec1a36 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -19,6 +19,39 @@ /** * Creation and modification timestamps of a node * + * * `created`: Date and time a node was created in its content stream + * * `originalCreated`: Date and time a node was created originally (this is equal to `created` for nodes in the original content stream) + * * `lastModified`: Date and time a node was last modified in its content stream (NULL = never modified) + * * `originalLastModified` Date and time a node was last modified in its original content stream + * + * When a node is originally created via one of the following events: + * * {@see RootNodeAggregateWithNodeWasCreated} + * * {@see NodeAggregateWithNodeWasCreated} + * the `created` and `originalCreated` values are set to the current date and time. + * + * When the creation even is re-applied on a different content stream (i.e. when publishing a node initially) + * or when a node variant is created via one of the following events: + * * {@see NodeSpecializationVariantWasCreated} + * * {@see NodeGeneralizationVariantWasCreated} + * * {@see NodePeerVariantWasCreated} + * the `created` value is set to the current date and time while the `originalCreated` value is copied over + * from the already existing node. + * + * The `lastModified` and `originalLastModified` values will be NULL upon creation. This allows for checking whether a node has been modified + * at all. + * + * When a node is being modified via one of the following events: + * * {@see NodeAggregateNameWasChanged} + * * {@see NodePropertiesWereSet} + * * {@see NodeReferencesWereSet} + * * {@see NodeAggregateWasEnabled} + * * {@see NodeAggregateTypeWasChanged} + * * {@see NodeAggregateWasMoved} + * * {@see NodeAggregateWasDisabled} + * the `lastModified` is set to the current date and time. + * the `originalLastModified` value is also set to the current date and time if the node is modified in the original content stream. + * Otherwise, it is copied over from the original event + * * @api */ final class Timestamps From 1fd3a8e64cd3a394499d8bba0020bd970a8af9ff Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Thu, 30 Mar 2023 17:57:05 +0200 Subject: [PATCH 08/12] Fix PublishMovedNodesWithoutDimensions tests Updating the `lastModified` timestamps might create new copies (copy on write) for moved nodes. Therefore the expected total number of nodes had to be adjusted in four scenarios --- .../PublishMovedNodesWithoutDimensions.feature | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature index b129711454b..e6ced9a7f8f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature @@ -82,7 +82,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 4 nodes + Then I expect the graph projection to consist of exactly 6 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -122,7 +122,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iii"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 4 nodes + Then I expect the graph projection to consist of exactly 6 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -172,7 +172,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 5 nodes + Then I expect the graph projection to consist of exactly 7 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -217,7 +217,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "nody-mc-nodeface"}] | When the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 4 nodes + Then I expect the graph projection to consist of exactly 6 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph From bb517de87db1951ff1d8052b6b9aafbc4be10ce4 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 31 Mar 2023 11:40:49 +0200 Subject: [PATCH 09/12] Don't update last modified timestamps on node move and dis/enable --- .../DoctrineDbalContentGraphProjection.php | 26 +--- .../Projection/Feature/NodeDisabling.php | 7 +- .../Domain/Projection/Feature/NodeMove.php | 7 +- ...PublishMovedNodesWithoutDimensions.feature | 8 +- .../Features/NodeTraversal/Timestamps.feature | 129 +++++++++++------- .../Projection/ContentGraph/Timestamps.php | 3 - 6 files changed, 93 insertions(+), 87 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 84043f316ea..9b0a648c057 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -221,7 +221,7 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch } elseif ($eventInstance instanceof NodeReferencesWereSet) { $this->whenNodeReferencesWereSet($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWasEnabled) { - $this->whenNodeAggregateWasEnabled($eventInstance, $eventEnvelope); + $this->whenNodeAggregateWasEnabled($eventInstance); } elseif ($eventInstance instanceof NodeAggregateTypeWasChanged) { $this->whenNodeAggregateTypeWasChanged($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof DimensionSpacePointWasMoved) { @@ -231,7 +231,7 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch } elseif ($eventInstance instanceof NodeAggregateWasRemoved) { $this->whenNodeAggregateWasRemoved($eventInstance); } elseif ($eventInstance instanceof NodeAggregateWasMoved) { - $this->whenNodeAggregateWasMoved($eventInstance, $eventEnvelope); + $this->whenNodeAggregateWasMoved($eventInstance); } elseif ($eventInstance instanceof NodeSpecializationVariantWasCreated) { $this->whenNodeSpecializationVariantWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeGeneralizationVariantWasCreated) { @@ -239,7 +239,7 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch } elseif ($eventInstance instanceof NodePeerVariantWasCreated) { $this->whenNodePeerVariantWasCreated($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof NodeAggregateWasDisabled) { - $this->whenNodeAggregateWasDisabled($eventInstance, $eventEnvelope); + $this->whenNodeAggregateWasDisabled($eventInstance); } else { throw new \RuntimeException('Not supported: ' . get_class($eventInstance)); } @@ -940,15 +940,14 @@ private function cascadeRestrictionRelations( /** * @throws \Throwable */ - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event, EventEnvelope $eventEnvelope): void + private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void { - $this->transactional(function () use ($event, $eventEnvelope) { + $this->transactional(function () use ($event) { $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( $event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints ); - $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, $event->affectedDimensionSpacePoints, $eventEnvelope); }); } @@ -1034,21 +1033,6 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { }); } - protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void - { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAndDimensionSpacePointsAndContentStream($nodeAggregateId, $affectedDimensionSpacePoints, $contentStreamId); - foreach ($anchorPoints as $anchorPoint) { - $this->updateNodeRecordWithCopyOnWrite( - $contentStreamId, - $anchorPoint, - function (NodeRecord $node) use ($eventEnvelope) { - $node->lastModified = $eventEnvelope->recordedAt; - $node->originalLastModified = self::initiatingDateTime($eventEnvelope); - } - ); - } - } - private function updateNodeRecordWithCopyOnWrite( ContentStreamId $contentStreamIdWhereWriteOccurs, NodeRelationAnchorPoint $anchorPoint, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php index 28ec7a3a948..05651879598 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php @@ -22,14 +22,12 @@ trait NodeDisabling { abstract protected function getTableNamePrefix(): string; - abstract protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void; - /** * @throws \Throwable */ - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event, EventEnvelope $eventEnvelope): void + private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void { - $this->transactional(function () use ($event, $eventEnvelope) { + $this->transactional(function () use ($event) { // TODO: still unsure why we need an "INSERT IGNORE" here; @@ -93,7 +91,6 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event, E 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY ] ); - $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, $event->affectedDimensionSpacePoints, $eventEnvelope); }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index de9a3398fe6..0f9721056b1 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -35,15 +35,13 @@ abstract protected function getProjectionContentGraph(): ProjectionContentGraph; abstract protected function getTableNamePrefix(): string; - abstract protected function updateLastModifiedTimestamp(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePointSet $affectedDimensionSpacePoints, EventEnvelope $eventEnvelope): void; - /** * @param NodeAggregateWasMoved $event * @throws \Throwable */ - private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEnvelope $eventEnvelope): void + private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { - $this->transactional(function () use ($event, $eventEnvelope) { + $this->transactional(function () use ($event) { $dimensionSpacePoints = []; @@ -107,7 +105,6 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event, EventEn ); } } - $this->updateLastModifiedTimestamp($event->nodeAggregateId, $event->contentStreamId, DimensionSpacePointSet::fromArray($dimensionSpacePoints), $eventEnvelope); }); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature index e6ced9a7f8f..b129711454b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePublishing/PublishMovedNodesWithoutDimensions.feature @@ -82,7 +82,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 6 nodes + Then I expect the graph projection to consist of exactly 4 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -122,7 +122,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iii"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 6 nodes + Then I expect the graph projection to consist of exactly 4 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -172,7 +172,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | And the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 7 nodes + Then I expect the graph projection to consist of exactly 5 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph @@ -217,7 +217,7 @@ Feature: Publishing moved nodes without dimensions | nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "nody-mc-nodeface"}] | When the graph projection is fully up to date - Then I expect the graph projection to consist of exactly 6 nodes + Then I expect the graph projection to consist of exactly 4 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph And I expect a node identified by cs-identifier;nody-mc-nodeface;{} to exist in the content graph diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index 4b671a434ee..c8addb6204e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -84,6 +84,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} | | a | a | Neos.ContentRepository.Testing:Page | home | {"text": "a"} | {} | | b | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | + And the current date and time is "2023-03-16T12:30:00+01:00" And the command CreateNodeVariant is executed with payload: | Key | Value | | contentStreamId | "cs-user" | @@ -109,7 +110,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When I am in content stream "cs-user" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | Scenario: NodeAggregateNameWasChanged events update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" @@ -127,7 +128,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When I am in content stream "cs-user" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | Scenario: NodeReferencesWereSet events update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" @@ -150,69 +151,68 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When I am in content stream "cs-user" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | And I expect the node "b" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - Scenario: NodeAggregateWasEnabled and NodeAggregateWasDisabled events update last modified timestamps + Scenario: NodeAggregateTypeWasChanged events 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 | - | contentStreamId | "cs-user" | - | coveredDimensionSpacePoint | {"language": "ch"} | - | nodeAggregateId | "a" | - | nodeVariantSelectionStrategy | "allSpecializations" | + And the command ChangeNodeAggregateType was published with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | + | strategy | "happypath" | And the graph projection is fully up to date And I am in content stream "cs-user" and dimension space point {"language":"de"} - And VisibilityConstraints are set to "withoutRestrictions" Then I expect the node "a" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | When I am in content stream "cs-user" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | - When the current date and time is "2023-03-16T14:00:00+01:00" - And the command EnableNodeAggregate is executed with payload: - | Key | Value | - | contentStreamId | "cs-user" | - | coveredDimensionSpacePoint | {"language": "ch"} | - | nodeAggregateId | "a" | - | nodeVariantSelectionStrategy | "allSpecializations" | + Scenario: NodePeerVariantWasCreated events set new created timestamps + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "home" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | And the graph projection is fully up to date And I am in content stream "cs-user" and dimension space point {"language":"de"} - Then I expect the node "a" to have the following timestamps: + Then I expect the node "home" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} - Then I expect the node "a" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + When I am in content stream "cs-user" and dimension space point {"language":"en"} + Then I expect the node "home" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | | | - Scenario: NodeAggregateTypeWasChanged events update last modified timestamps + Scenario: NodeGeneralizationVariantWasCreated events set new created timestamps When the current date and time is "2023-03-16T13:00:00+01:00" - And the command ChangeNodeAggregateType was published with payload: - | Key | Value | - | contentStreamId | "cs-user" | - | nodeAggregateId | "a" | - | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | - | strategy | "happypath" | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "home" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"mul"} | And the graph projection is fully up to date And I am in content stream "cs-user" and dimension space point {"language":"de"} - Then I expect the node "a" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + Then I expect the node "home" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"mul"} + Then I expect the node "home" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} - Then I expect the node "a" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | - Scenario: NodeAggregateWasMoved events update last modified timestamps + Scenario: NodeAggregateWasMoved events don't update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" And the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -226,18 +226,11 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - And I expect the node "b" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | When I am in content stream "cs-user" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: - | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | - And I expect the node "b" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | Scenario: RootNodeAggregateDimensionsWereUpdated events don't update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" @@ -251,10 +244,48 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | + + Scenario: NodeAggregateWasEnabled and NodeAggregateWasDisabled 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 | + | contentStreamId | "cs-user" | + | coveredDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} + And VisibilityConstraints are set to "withoutRestrictions" + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | + + When the current date and time is "2023-03-16T14:00:00+01:00" + And the command EnableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | coveredDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And I am in content stream "cs-user" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + When I am in content stream "cs-user" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | + Scenario: Original created and last modified timestamps when publishing nodes over multiple content streams When the current date and time is "2023-03-16T13:00:00+01:00" diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php index c5ce2ec1a36..96c2550225f 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -44,10 +44,7 @@ * * {@see NodeAggregateNameWasChanged} * * {@see NodePropertiesWereSet} * * {@see NodeReferencesWereSet} - * * {@see NodeAggregateWasEnabled} * * {@see NodeAggregateTypeWasChanged} - * * {@see NodeAggregateWasMoved} - * * {@see NodeAggregateWasDisabled} * the `lastModified` is set to the current date and time. * the `originalLastModified` value is also set to the current date and time if the node is modified in the original content stream. * Otherwise, it is copied over from the original event From 4aa12d62525b1c2aaa39237aa966a85ca758751c Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 31 Mar 2023 11:54:43 +0200 Subject: [PATCH 10/12] Refactor NodeRecord helper --- .../DoctrineDbalContentGraphProjection.php | 49 ++++++++++++------- .../src/Domain/Projection/NodeRecord.php | 28 +++++------ .../Projection/ContentGraph/Timestamps.php | 21 ++++++++ 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 9b0a648c057..d4be2c06eea 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -45,6 +45,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; @@ -289,10 +290,12 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo $event->nodeTypeName, $event->nodeAggregateClassification, null, - $eventEnvelope->recordedAt, - self::initiatingDateTime($eventEnvelope), - null, - null, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); $this->transactional(function () use ($node, $event) { @@ -475,10 +478,12 @@ private function createNodeWithHierarchy( $nodeTypeName, $nodeAggregateClassification, $nodeName, - $eventEnvelope->recordedAt, - self::initiatingDateTime($eventEnvelope), - null, - null, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); // reconnect parent relations @@ -780,8 +785,10 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties->merge($event->propertyValues); - $node->lastModified = $eventEnvelope->recordedAt; - $node->originalLastModified = self::initiatingDateTime($eventEnvelope); + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); } ); }); @@ -815,8 +822,10 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn $event->contentStreamId, $nodeAnchorPoint, function (NodeRecord $node) use ($eventEnvelope) { - $node->lastModified = $eventEnvelope->recordedAt; - $node->originalLastModified = self::initiatingDateTime($eventEnvelope); + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); } ); @@ -1005,10 +1014,12 @@ protected function copyNodeToDimensionSpacePoint( $sourceNode->nodeTypeName, $sourceNode->classification, $sourceNode->nodeName, - $eventEnvelope->recordedAt, - self::initiatingDateTime($eventEnvelope), - null, - null, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); @@ -1025,8 +1036,10 @@ private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $ev $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; - $node->lastModified = $eventEnvelope->recordedAt; - $node->originalLastModified = self::initiatingDateTime($eventEnvelope); + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); } ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index b1086d03210..455397d4720 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -17,6 +17,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Types\Types; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -40,10 +41,7 @@ public function __construct( public NodeAggregateClassification $classification, /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ public ?NodeName $nodeName, - public \DateTimeImmutable $created, - public \DateTimeImmutable $originalCreated, - public ?\DateTimeImmutable $lastModified, - public ?\DateTimeImmutable $originalLastModified, + public Timestamps $timestamps, ) { } @@ -61,10 +59,10 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, 'classification' => $this->classification->value, - 'created' => $this->created, - 'originalcreated' => $this->originalCreated, - 'lastmodified' => $this->lastModified, - 'originallastmodified' => $this->originalLastModified, + 'created' => $this->timestamps->created, + 'originalcreated' => $this->timestamps->originalCreated, + 'lastmodified' => $this->timestamps->lastModified, + 'originallastmodified' => $this->timestamps->originalLastModified, ], [ 'created' => Types::DATETIME_IMMUTABLE, 'originalcreated' => Types::DATETIME_IMMUTABLE, @@ -88,8 +86,8 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa 'properties' => json_encode($this->properties), 'nodetypename' => (string)$this->nodeTypeName, 'classification' => $this->classification->value, - 'lastmodified' => $this->lastModified, - 'originallastmodified' => $this->originalLastModified, + 'lastmodified' => $this->timestamps->lastModified, + 'originallastmodified' => $this->timestamps->originalLastModified, ], [ 'relationanchorpoint' => $this->relationAnchorPoint @@ -128,10 +126,12 @@ public static function fromDatabaseRow(array $databaseRow): self NodeTypeName::fromString($databaseRow['nodetypename']), NodeAggregateClassification::from($databaseRow['classification']), isset($databaseRow['name']) ? NodeName::fromString($databaseRow['name']) : null, - self::parseDateTimeString($databaseRow['created']), - self::parseDateTimeString($databaseRow['originalcreated']), - isset($databaseRow['lastmodified']) ? self::parseDateTimeString($databaseRow['lastmodified']) : null, - isset($databaseRow['originallastmodified']) ? self::parseDateTimeString($databaseRow['originallastmodified']) : null, + Timestamps::create( + self::parseDateTimeString($databaseRow['created']), + self::parseDateTimeString($databaseRow['originalcreated']), + isset($databaseRow['lastmodified']) ? self::parseDateTimeString($databaseRow['lastmodified']) : null, + isset($databaseRow['originallastmodified']) ? self::parseDateTimeString($databaseRow['originallastmodified']) : null, + ), ); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php index 96c2550225f..314f5bf1a46 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -75,4 +75,25 @@ public static function create( ): self { return new self($created, $originalCreated, $lastModified, $originalLastModified); } + + /** + * Returns a new copy with the specified new values + * + * Note: The signature of this method might be extended in the future, so it should always be used with named arguments + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + */ + public function with( + DateTimeImmutable $created = null, + DateTimeImmutable $originalCreated = null, + DateTimeImmutable $lastModified = null, + DateTimeImmutable $originalLastModified = null, + ) + { + return new self( + $created ?? $this->created, + $originalCreated ?? $this->originalCreated, + $lastModified ?? $this->lastModified, + $originalLastModified ?? $this->originalLastModified, + ); + } } From 773f9c66cb4ff4d9c284dd2adf9b436af9abc183 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 31 Mar 2023 12:20:39 +0200 Subject: [PATCH 11/12] Fix code style --- .../src/DoctrineDbalContentGraphProjection.php | 2 +- .../Classes/Projection/ContentGraph/Timestamps.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index d4be2c06eea..65a965523b1 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -293,7 +293,7 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo Timestamps::create( $eventEnvelope->recordedAt, self::initiatingDateTime($eventEnvelope), - null, + null, null, ), ); diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php index 314f5bf1a46..a09f9faf1d6 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -87,8 +87,7 @@ public function with( DateTimeImmutable $originalCreated = null, DateTimeImmutable $lastModified = null, DateTimeImmutable $originalLastModified = null, - ) - { + ): self { return new self( $created ?? $this->created, $originalCreated ?? $this->originalCreated, From 32bd412e0e1196dfd8cbb9ff902f220c91bbe419 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Mon, 3 Apr 2023 13:40:17 +0200 Subject: [PATCH 12/12] Remove unused code from DbalAdapter --- .../Domain/Projection/Feature/NodeMove.php | 4 --- .../Repository/ProjectionContentGraph.php | 30 ------------------- 2 files changed, 34 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index 0f9721056b1..f2e032259f7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -42,9 +42,6 @@ abstract protected function getTableNamePrefix(): string; private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { $this->transactional(function () use ($event) { - - $dimensionSpacePoints = []; - foreach ($event->nodeMoveMappings as $moveNodeMapping) { // for each materialized node in the DB which we want to adjust, we have one MoveNodeMapping. /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveNodeMapping */ @@ -61,7 +58,6 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void foreach ($moveNodeMapping->newLocations as $newLocation) { assert($newLocation instanceof CoverageNodeMoveMapping); - $dimensionSpacePoints[] = $newLocation->coveredDimensionSpacePoint; $affectedDimensionSpacePoints = new DimensionSpacePointSet([ $newLocation->coveredDimensionSpacePoint ]); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 927daa1bdd8..88204fbda02 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -181,36 +181,6 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea return null; } - /** - * @return iterable - */ - public function getAnchorPointsForNodeAndDimensionSpacePointsAndContentStream( - NodeAggregateId $nodeAggregateId, - DimensionSpacePointSet $dimensionSpacePoints, - ContentStreamId $contentStreamId - ): iterable { - $rows = $this->getDatabaseConnection()->executeQuery( - 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint - WHERE n.nodeaggregateid = :nodeAggregateId - AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes) - AND h.contentstreamid = :contentStreamId', - [ - 'nodeAggregateId' => (string)$nodeAggregateId, - 'dimensionSpacePointHashes' => $dimensionSpacePoints->getPointHashes(), - 'contentStreamId' => (string)$contentStreamId, - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, - ] - )->fetchAllAssociative(); - - return array_map( - static fn(array $row) => NodeRelationAnchorPoint::fromString($row['relationanchorpoint']), - $rows - ); - } - /** * @param NodeAggregateId $nodeAggregateId * @param ContentStreamId $contentStreamId