diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 36c24163ace..9cd2fcef620 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.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 5598c44c7a3..65a965523b1 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; @@ -21,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; @@ -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; @@ -205,25 +206,25 @@ 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 RootNodeAggregateDimensionsWereUpdated) { $this->whenRootNodeAggregateDimensionsWereUpdated($eventInstance); } 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); } elseif ($eventInstance instanceof NodeAggregateTypeWasChanged) { - $this->whenNodeAggregateTypeWasChanged($eventInstance); + $this->whenNodeAggregateTypeWasChanged($eventInstance, $eventEnvelope); } elseif ($eventInstance instanceof DimensionSpacePointWasMoved) { $this->whenDimensionSpacePointWasMoved($eventInstance); } elseif ($eventInstance instanceof DimensionShineThroughWasAdded) { @@ -233,11 +234,11 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch } elseif ($eventInstance instanceof NodeAggregateWasMoved) { $this->whenNodeAggregateWasMoved($eventInstance); } 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); } else { @@ -276,7 +277,7 @@ public function markStale(): void /** * @throws \Throwable */ - private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event): void + private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { $nodeRelationAnchorPoint = NodeRelationAnchorPoint::create(); $originDimensionSpacePoint = OriginDimensionSpacePoint::fromArray([]); @@ -287,7 +288,14 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo $originDimensionSpacePoint->hash, SerializedPropertyValues::fromArray([]), $event->nodeTypeName, - $event->nodeAggregateClassification + $event->nodeAggregateClassification, + null, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); $this->transactional(function () use ($node, $event) { @@ -346,9 +354,9 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim /** * @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, @@ -359,7 +367,8 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre $event->initialPropertyValues, $event->nodeAggregateClassification, $event->succeedingNodeAggregateId, - $event->nodeName + $event->nodeName, + $eventEnvelope, ); $this->connectRestrictionRelationsFromParentNodeToNewlyCreatedNode( @@ -374,22 +383,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.lastmodified = :lastModified, + n.originallastmodified = :originalLastModified + WHERE n.nodeaggregateid = :nodeAggregateId and h.contentstreamid = :contentStreamId ', [ 'newName' => (string)$event->newNodeName, 'nodeAggregateId' => (string)$event->nodeAggregateId, - 'contentStreamId' => (string)$event->contentStreamId + 'contentStreamId' => (string)$event->contentStreamId, + 'lastModified' => $eventEnvelope->recordedAt, + 'originalLastModified' => self::initiatingDateTime($eventEnvelope), + ], [ + 'lastModified' => Types::DATETIME_IMMUTABLE, + 'originalLastModified' => Types::DATETIME_IMMUTABLE, ]); }); } @@ -447,8 +464,9 @@ 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(); $node = new NodeRecord( @@ -459,7 +477,13 @@ private function createNodeWithHierarchy( $propertyDefaultValuesAndTypes, $nodeTypeName, $nodeAggregateClassification, - $nodeName + $nodeName, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); // reconnect parent relations @@ -508,7 +532,7 @@ private function createNodeWithHierarchy( * @param DimensionSpacePointSet $dimensionSpacePointSet * @throws \Doctrine\DBAL\DBALException */ - protected function connectHierarchy( + private function connectHierarchy( ContentStreamId $contentStreamId, NodeRelationAnchorPoint $parentNodeAnchorPoint, NodeRelationAnchorPoint $childNodeAnchorPoint, @@ -548,7 +572,7 @@ protected function connectHierarchy( * @return int * @throws \Doctrine\DBAL\DBALException */ - protected function getRelationPosition( + private function getRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, @@ -585,7 +609,7 @@ protected function getRelationPosition( * @return int * @throws \Doctrine\DBAL\DBALException */ - protected function getRelationPositionAfterRecalculation( + private function getRelationPositionAfterRecalculation( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, @@ -631,7 +655,7 @@ protected function getRelationPositionAfterRecalculation( /** * @throws \Throwable */ - public function whenContentStreamWasForked(ContentStreamWasForked $event): void + private function whenContentStreamWasForked(ContentStreamWasForked $event): void { $this->transactional(function () use ($event) { @@ -690,7 +714,7 @@ public function whenContentStreamWasForked(ContentStreamWasForked $event): void }); } - public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void + private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { $this->transactional(function () use ($event) { @@ -739,21 +763,43 @@ public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): voi /** * @throws \Throwable */ - public function whenNodePropertiesWereSet(NodePropertiesWereSet $event): void + private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { - $this->updateNodeWithCopyOnWrite($event, function (NodeRecord $node) use ($event) { - $node->properties = $node->properties->merge($event->propertyValues); - }); + $this->transactional(function () use ($event, $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->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); }); } /** * @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( @@ -775,7 +821,11 @@ public function whenNodeReferencesWereSet(NodeReferencesWereSet $event): void $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, $nodeAnchorPoint, - function (NodeRecord $node) { + function (NodeRecord $node) use ($eventEnvelope) { + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); } ); @@ -899,7 +949,7 @@ private function cascadeRestrictionRelations( /** * @throws \Throwable */ - public function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void + private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void { $this->transactional(function () use ($event) { $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( @@ -951,7 +1001,8 @@ protected function copyHierarchyRelationToDimensionSpacePoint( */ protected function copyNodeToDimensionSpacePoint( NodeRecord $sourceNode, - OriginDimensionSpacePoint $originDimensionSpacePoint + OriginDimensionSpacePoint $originDimensionSpacePoint, + EventEnvelope $eventEnvelope, ): NodeRecord { $copyRelationAnchorPoint = NodeRelationAnchorPoint::create(); $copy = new NodeRecord( @@ -962,76 +1013,40 @@ protected function copyNodeToDimensionSpacePoint( $sourceNode->properties, $sourceNode->nodeTypeName, $sourceNode->classification, - $sourceNode->nodeName + $sourceNode->nodeName, + Timestamps::create( + $eventEnvelope->recordedAt, + self::initiatingDateTime($eventEnvelope), + null, + null, + ), ); $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); return $copy; } - public function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event): void + private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void { - $this->transactional(function () use ($event) { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( - $event->nodeAggregateId, - $event->contentStreamId - ); - + $this->transactional(function () use ($event, $eventEnvelope) { + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $event->contentStreamId); foreach ($anchorPoints as $anchorPoint) { $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, $anchorPoint, - function (NodeRecord $node) use ($event) { + function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); } ); } }); } - /** - * @throws \Doctrine\DBAL\DBALException - * @throws \Exception - */ - protected function updateNodeWithCopyOnWrite(EventInterface $event, callable $operations): mixed - { - 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 - ); - } - - return $this->updateNodeRecordWithCopyOnWrite( - $event->getContentStreamId(), - $anchorPoint, - $operations - ); - } - - protected function updateNodeRecordWithCopyOnWrite( + private function updateNodeRecordWithCopyOnWrite( ContentStreamId $contentStreamIdWhereWriteOccurs, NodeRelationAnchorPoint $anchorPoint, callable $operations @@ -1093,7 +1108,7 @@ protected function updateNodeRecordWithCopyOnWrite( } - protected function copyReferenceRelations( + private function copyReferenceRelations( NodeRelationAnchorPoint $sourceRelationAnchorPoint, NodeRelationAnchorPoint $destinationRelationAnchorPoint ): void { @@ -1118,7 +1133,7 @@ protected 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 @@ -1192,7 +1207,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 @@ -1256,13 +1271,22 @@ 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(); } + + 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/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 0fa11486831..5368bc7fb33 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('created', Types::DATETIME_IMMUTABLE) + ->setNotnull(true); + $table->addColumn('originalcreated', Types::DATETIME_IMMUTABLE) + ->setNotnull(true); + $table->addColumn('lastmodified', Types::DATETIME_IMMUTABLE) + ->setNotnull(false) + ->setDefault(null); + $table->addColumn('originallastmodified', 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/NodeDisabling.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php index bb68b36dd67..05651879598 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php @@ -5,7 +5,13 @@ 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; /** * The NodeDisabling projection feature trait @@ -22,6 +28,8 @@ abstract protected function getTableNamePrefix(): string; private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void { $this->transactional(function () use ($event) { + + // TODO: still unsure why we need an "INSERT IGNORE" here; // normal "INSERT" can trigger a duplicate key constraint exception $this->getDatabaseConnection()->executeStatement( diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index e76f64d00de..f2e032259f7 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 @@ -57,13 +58,15 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void foreach ($moveNodeMapping->newLocations as $newLocation) { assert($newLocation instanceof CoverageNodeMoveMapping); + $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) @@ -94,9 +97,7 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void $event->contentStreamId, $newParentNodeAggregateId, $event->nodeAggregateId, - new DimensionSpacePointSet([ - $newLocation->coveredDimensionSpacePoint - ]) + $affectedDimensionSpacePoints ); } } 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..455397d4720 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -15,7 +15,9 @@ 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\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -38,7 +40,8 @@ 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 Timestamps $timestamps, ) { } @@ -55,7 +58,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, + 'created' => $this->timestamps->created, + 'originalcreated' => $this->timestamps->originalCreated, + 'lastmodified' => $this->timestamps->lastModified, + 'originallastmodified' => $this->timestamps->originalLastModified, + ], [ + 'created' => Types::DATETIME_IMMUTABLE, + 'originalcreated' => Types::DATETIME_IMMUTABLE, + 'lastmodified' => Types::DATETIME_IMMUTABLE, + 'originallastmodified' => Types::DATETIME_IMMUTABLE, ]); } @@ -73,10 +85,16 @@ 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, + 'lastmodified' => $this->timestamps->lastModified, + 'originallastmodified' => $this->timestamps->originalLastModified, ], [ 'relationanchorpoint' => $this->relationAnchorPoint + ], + [ + 'lastmodified' => Types::DATETIME_IMMUTABLE, + 'originallastmodified' => Types::DATETIME_IMMUTABLE, ] ); } @@ -107,7 +125,22 @@ 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, + 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, + ), ); } + + 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 afba73d609a..4a528a06c52 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,6 +78,12 @@ public function mapNodeRowToNode( $this->nodeTypeManager->getNodeType($nodeRow['nodetypename']), $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, + Timestamps::create( + self::parseDateTimeString($nodeRow['created']), + self::parseDateTimeString($nodeRow['originalcreated']), + isset($nodeRow['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, + isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, + ), ); } @@ -306,4 +313,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.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 33c02d07702..88204fbda02 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -177,9 +177,8 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea if (count($rows) === 1) { return NodeRelationAnchorPoint::fromString($rows[0]['relationanchorpoint']); - } else { - return null; } + return null; } /** diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 6387fb44a07..2f33aefbf95 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,6 +89,13 @@ public function mapNodeRowToNode( $this->propertyConverter ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, + Timestamps::create( + // 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['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, + isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, + ), ); return $result; @@ -340,4 +348,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.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Bootstrap/FeatureContext.php index ef58483a56c..8d734469293 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/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature new file mode 100644 index 00000000000..c8addb6204e --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -0,0 +1,332 @@ +@contentrepository @adapters=DoctrineDBAL + # TODO implement for Postgres +Feature: Behavior of Node timestamp properties "created", "originalCreated", "lastModified" and "originalLastModified" + + Background: + 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: + """ + '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 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 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" | + | 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: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" + 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: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" + 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: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: 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:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + + 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 "home" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + + When I am in content stream "cs-user" and dimension space point {"language":"en"} + 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: NodeGeneralizationVariantWasCreated 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":"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 "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 | | | + + + 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 | + | 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 | | | + + 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: 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: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" + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-user" | + | nodeAggregateId | "a" | + | propertyValues | {"text": "Changed"} | + 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" + And the command PublishWorkspace is executed with payload: + | 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: + | 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 | | | + + And I am in content stream "cs-review" + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | + 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 | | | diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index bc68db3d8c8..e1ab60571a0 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 @@ -117,20 +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), - ); - } - /** * @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 6be65366d85..ba0f8f558f6 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -27,12 +27,12 @@ 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; use Symfony\Component\Serializer\Serializer; /** @@ -54,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( @@ -97,7 +98,8 @@ public function build(): ContentRepository $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, - $this->userIdProvider + $this->userIdProvider, + $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 935d6530777..09e22a5b48a 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -67,7 +67,8 @@ 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 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..a09f9faf1d6 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Timestamps.php @@ -0,0 +1,98 @@ +created, + $originalCreated ?? $this->originalCreated, + $lastModified ?? $this->lastModified, + $originalLastModified ?? $this->originalLastModified, + ); + } +} 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..bec52047125 --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentDateTimeTrait.php @@ -0,0 +1,29 @@ +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 f6b8ca9e5a2..cfd771fba87 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/EventSourcedTrait.php @@ -34,30 +34,23 @@ 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; @@ -76,13 +69,20 @@ 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; +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; /** @@ -92,6 +92,7 @@ trait EventSourcedTrait { use CurrentSubgraphTrait; use CurrentUserTrait; + use CurrentDateTimeTrait; use NodeTraversalTrait; use ProjectedNodeAggregateTrait; use ProjectedNodeTrait; @@ -140,8 +141,7 @@ protected function getContentRepositoryRegistry(): ContentRepositoryRegistry protected function getContentRepository(): ContentRepository { - $currentUserId = $this->getCurrentUserId(); - return $currentUserId === null ? $this->contentRepository : $this->contentRepository->withInitiatingUserId($currentUserId); + return $this->contentRepository; } /** @@ -242,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) { @@ -373,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. @@ -521,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) { @@ -548,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 22b5327f53d..5dec212ee70 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/Features/NodeCreation.php @@ -44,8 +44,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 @@ +[^"]*)"(?: and filter '(?[^']*)')? I expect (?:the nodes "(?[^"]*)"|no nodes) to be returned( and the total count to be (?\d+))?$/ */ @@ -396,4 +394,26 @@ public function iExecuteTheCountNodesQueryIExpectTheFollowingResult(int $expecte } } + /** + * @Then I expect the node :nodeIdSerialized to have the following timestamps: + */ + public function iExpectTheNodeToHaveTheFollowingTimestamps(string $nodeIdSerialized, TableNode $expectedTimestampsTable): void + { + $nodeAggregateId = NodeAggregateId::fromString($nodeIdSerialized); + $expectedTimestamps = array_map(static fn (string $timestamp) => $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 = [ + 'created' => $node->timestamps->created, + 'originalCreated' => $node->timestamps->originalCreated, + 'lastModified' => $node->timestamps->lastModified, + 'originalLastModified' => $node->timestamps->originalLastModified, + ]; + Assert::assertEquals($expectedTimestamps, $actualTimestamps); + } + } } 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", diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index c6730f1cdf0..6c9675f1b97 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/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 162e555cb78..4ea31967907 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,7 +101,6 @@ public function getService(ContentRepositoryId $contentRepositoryId, ContentRepo return $this->contentRepositoryServiceInstances[$contentRepositoryId->value][get_class($contentRepositoryServiceFactory)]; } - /** * @throws ContentRepositoryNotFound | InvalidConfigurationException */ @@ -126,29 +128,31 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content assert(isset($this->settings['presets'][$contentRepositorySettings['preset']]) && is_array($this->settings['presets'][$contentRepositorySettings['preset']]), InvalidConfigurationException::missingPreset($contentRepositoryId, $contentRepositorySettings['preset'])); $contentRepositoryPreset = $this->settings['presets'][$contentRepositorySettings['preset']]; try { + $clock = $this->buildClock($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset); return new ContentRepositoryFactory( $contentRepositoryId, - $this->buildEventStore($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), + $this->buildEventStore($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset, $clock), $this->buildNodeTypeManager($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), $this->buildContentDimensionSource($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), $this->buildPropertySerializer($contentRepositorySettings, $contentRepositoryPreset), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), $this->buildProjectionCatchUpTrigger($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset), + $clock, ); } catch (\Exception $exception) { throw InvalidConfigurationException::fromException($contentRepositoryId, $exception); } } - private function buildEventStore(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $contentRepositoryPreset): EventStoreInterface + private function buildEventStore(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $contentRepositoryPreset, ClockInterface $clock): EventStoreInterface { assert(isset($contentRepositoryPreset['eventStore']['factoryObjectName']), InvalidConfigurationException::fromMessage('Content repository preset "%s" does not have eventStore.factoryObjectName configured.', $contentRepositorySettings['preset'])); $eventStoreFactory = $this->objectManager->get($contentRepositoryPreset['eventStore']['factoryObjectName']); if (!$eventStoreFactory instanceof EventStoreFactoryInterface) { throw new \RuntimeException(sprintf('eventStore.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, EventStoreFactoryInterface::class, get_debug_type($eventStoreFactory))); } - return $eventStoreFactory->build($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset['eventStore']); + return $eventStoreFactory->build($contentRepositoryId, $contentRepositorySettings, $contentRepositoryPreset['eventStore'], $clock); } private function buildNodeTypeManager(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $contentRepositoryPreset): NodeTypeManager @@ -239,4 +243,14 @@ private function buildUserIdProvider(ContentRepositoryId $contentRepositoryId, a } return $userIdProviderFactory->build($contentRepositoryId, $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($contentRepositoryIdentifier, $contentRepositorySettings, $contentRepositoryPreset); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php new file mode 100644 index 00000000000..4d984510e57 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/Clock/ClockFactoryInterface.php @@ -0,0 +1,14 @@ +connection, - self::databaseTableName($contentRepositoryId) + self::databaseTableName($contentRepositoryId), + $clock ); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/EventStoreFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/EventStoreFactoryInterface.php index 4eba3caa7e3..326a448698e 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/EventStoreFactoryInterface.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/EventStoreFactoryInterface.php @@ -4,8 +4,9 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\EventStore\EventStoreInterface; +use Psr\Clock\ClockInterface; interface EventStoreFactoryInterface { - public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $eventStorePreset): EventStoreInterface; + public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $eventStorePreset, ClockInterface $clock): EventStoreInterface; } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php index 4a857027fa4..6624f0fd7f6 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php @@ -7,9 +7,12 @@ use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; +/** + * @api + */ final class StaticUserIdProviderFactory implements UserIdProviderFactoryInterface { - public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $projectionCatchUpTriggerPreset): UserIdProviderInterface + public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $contentRepositoryPreset): UserIdProviderInterface { return new StaticUserIdProvider(UserId::forSystemUser()); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php index f57fc100426..340dc52c180 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php @@ -5,7 +5,10 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; +/** + * @api + */ interface UserIdProviderFactoryInterface { - public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $projectionCatchUpTriggerPreset): UserIdProviderInterface; + public function build(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings, array $contentRepositoryPreset): UserIdProviderInterface; } diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index 0f23000a533..44117db2c43 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -33,6 +33,9 @@ Neos: userIdProvider: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\UserIdProvider\StaticUserIdProviderFactory + clock: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\Clock\SystemClockFactory + propertyConverters: DateTimeNormalizer: className: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer diff --git a/Neos.ContentRepositoryRegistry/composer.json b/Neos.ContentRepositoryRegistry/composer.json index a896c97c891..e83b72a38cf 100644 --- a/Neos.ContentRepositoryRegistry/composer.json +++ b/Neos.ContentRepositoryRegistry/composer.json @@ -8,7 +8,8 @@ "require": { "neos/flow": "*", "neos/contentrepository-core": "*", - "symfony/property-access": "*" + "symfony/property-access": "*", + "psr/clock": "^1" }, "autoload": { "psr-4": { diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 007c31f0e06..ee50887384b 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'); diff --git a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php index 6905f7c8031..2c6852d3679 100644 --- a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php +++ b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\PropertyCollectionInterface; +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\DimensionSpace\OriginDimensionSpacePoint; @@ -148,6 +149,8 @@ protected function setUp(): void return null; }); + $now = new \DateTimeImmutable(); + $this->textNode = new Node( ContentSubgraphIdentity::create( ContentRepositoryId::fromString("cr"), @@ -161,7 +164,8 @@ protected function setUp(): void NodeTypeName::fromString("nt"), $nodeType, $textNodeProperties, - null + null, + Timestamps::create($now, $now, null, null) ); } }