Skip to content

Commit

Permalink
!!! BUGFIX: DocumentUriProjection doesn't respect fallback nodes; Nod…
Browse files Browse the repository at this point in the history
…ePropertiesWereSet contains affectedDimensionSpacePoints

Extending the NodePropertiesWereSet event is useful for some projections
which are not interested in OriginDimensionSpacePoints, but need to know
where a property value is applied (like the DocumentUriPathProjection).

This change is BREAKING because it updates the event payload of
NodePropertiesWereSet. To update your event store, run:

./flow migrateEvents:fillAffectedDimensionSpacePointsInNodePropertiesWereSet

We also fix the DocumentUriPathProjection alongside.

Resolves: #4265
Resolves: #4256
  • Loading branch information
skurfuerst authored and weblate committed May 10, 2023
1 parent 57203c0 commit 782b958
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace Neos\ContentRepository\Core\Feature\NodeModification\Event;

use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet;
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId;
Expand Down Expand Up @@ -41,7 +42,9 @@ final class NodePropertiesWereSet implements
public function __construct(
public readonly ContentStreamId $contentStreamId,
public readonly NodeAggregateId $nodeAggregateId,
public readonly OriginDimensionSpacePoint $originDimensionSpacePoint, // TODO
public readonly OriginDimensionSpacePoint $originDimensionSpacePoint,
/** the covered dimension space points for this modification - i.e. where this change is visible */
public readonly DimensionSpacePointSet $affectedDimensionSpacePoints,
public readonly SerializedPropertyValues $propertyValues
) {
}
Expand All @@ -67,6 +70,7 @@ public function createCopyForContentStream(ContentStreamId $targetContentStreamI
$targetContentStreamId,
$this->nodeAggregateId,
$this->originDimensionSpacePoint,
$this->affectedDimensionSpacePoints,
$this->propertyValues
);
}
Expand All @@ -77,6 +81,7 @@ public function mergeProperties(self $other): self
$this->contentStreamId,
$this->nodeAggregateId,
$this->originDimensionSpacePoint,
$this->affectedDimensionSpacePoints,
$this->propertyValues->merge($other->propertyValues)
);
}
Expand All @@ -87,6 +92,7 @@ public static function fromArray(array $values): EventInterface
ContentStreamId::fromString($values['contentStreamId']),
NodeAggregateId::fromString($values['nodeAggregateId']),
OriginDimensionSpacePoint::fromArray($values['originDimensionSpacePoint']),
DimensionSpacePointSet::fromArray($values['affectedDimensionSpacePoints']),
SerializedPropertyValues::fromArray($values['propertyValues']),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private function handleSetSerializedNodeProperties(
$command->contentStreamId,
$command->nodeAggregateId,
$affectedOrigin,
$nodeAggregate->getCoverageByOccupant($affectedOrigin),
$propertyValues,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);

namespace Neos\ContentRepositoryRegistry\Command;

use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\ContentRepositoryRegistry\Service\EventMigrationServiceFactory;
use Neos\Flow\Cli\CommandController;

final class MigrateEventsCommandController extends CommandController
{

public function __construct(
private readonly ContentRepositoryRegistry $contentRepositoryRegistry,
private readonly EventMigrationServiceFactory $eventMigrationServiceFactory,
) {
parent::__construct();
}

/**
* Adds affectedDimensionSpacePoints to NodePropertiesWereSet event, by replaying the content graph
* and then reading the dimension space points for the relevant NodeAggregate.
*
* Needed for #4265: https://github.com/neos/neos-development-collection/issues/4265
*
* Included in May 2023 - before Neos 9.0 Beta 1.
*
* @param string $contentRepository Identifier of the Content Repository to set up
*/
public function fillAffectedDimensionSpacePointsInNodePropertiesWereSetCommand(string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$eventMigrationService = $this->contentRepositoryRegistry->getService($contentRepositoryId, $this->eventMigrationServiceFactory);
$eventMigrationService->fillAffectedDimensionSpacePointsInNodePropertiesWereSet($this->outputLine(...));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);

namespace Neos\ContentRepositoryRegistry\Service;

use Doctrine\DBAL\Connection;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjection;
use Neos\ContentRepository\Core\Projection\Projections;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepositoryRegistry\Command\MigrateEventsCommandController;
use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory;
use Neos\EventStore\EventStoreInterface;
use Neos\EventStore\Model\Event\SequenceNumber;
use Neos\EventStore\Model\EventStream\VirtualStreamName;
use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection;

/**
* Content Repository service to perform migrations of events.
*
* Each function is used here for a specific migration. The migrations are only useful for production
* workloads which have events prior to the code change.
*
* @internal this is currently only used by the {@see MigrateEventsCommandController}
*/
final class EventMigrationService implements ContentRepositoryServiceInterface
{

public function __construct(
private readonly Projections $projections,
private readonly ContentRepositoryId $contentRepositoryId,
private readonly ContentRepository $contentRepository,
private readonly EventStoreInterface $eventStore,
private readonly Connection $connection,
) {
}

/**
* Adds affectedDimensionSpacePoints to NodePropertiesWereSet event, by replaying the content graph
* and then reading the dimension space points for the relevant NodeAggregate.
*
* Needed for #4265: https://github.com/neos/neos-development-collection/issues/4265
*
* Included in May 2023 - before Neos 9.0 Beta 1.
*
* @param \Closure $outputFn
* @return void
*/
public function fillAffectedDimensionSpacePointsInNodePropertiesWereSet(\Closure $outputFn)
{

$backupEventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId)
. '_bak_' . date('Y_m_d_H_i_s');
$outputFn('Backup: copying events table to %s', [$backupEventTableName]);
$this->copyEventTable($backupEventTableName);

$outputFn('Backup completed. Resetting Graph Projection.');
$this->contentRepository->resetProjectionState(ContentGraphProjection::class);

$contentGraphProjection = $this->projections->get(ContentGraphProjection::class);
$contentGraph = $contentGraphProjection->getState();
assert($contentGraph instanceof ContentGraphInterface);

$streamName = VirtualStreamName::all();
$eventStream = $this->eventStore->load($streamName);
foreach ($eventStream as $eventEnvelope) {
if ($eventEnvelope->event->type->value === 'NodePropertiesWereSet') {
$eventData = json_decode($eventEnvelope->event->data->value, true);
if (!isset($eventData['affectedDimensionSpacePoints'])) {
// Replay the projection until before the current NodePropertiesWereSet event
$contentGraphProjection->catchUp(
$eventStream->withMaximumSequenceNumber($eventEnvelope->sequenceNumber->previous()),
$this->contentRepository
);

// now we can ask the NodeAggregate (read model) for the covered DSPs.
$nodeAggregate = $contentGraph->findNodeAggregateById(
ContentStreamId::fromString($eventData['contentStreamId']),
NodeAggregateId::fromString($eventData['nodeAggregateId'])
);
$affectedDimensionSpacePoints = $nodeAggregate->getCoverageByOccupant(
OriginDimensionSpacePoint::fromArray($eventData['originDimensionSpacePoint'])
);

// ... and update the event
$eventData['affectedDimensionSpacePoints'] = $affectedDimensionSpacePoints->jsonSerialize();
$outputFn(
'Rewriting %s: (%s, Origin: %s) => Affected: %s',
[
$eventEnvelope->sequenceNumber->value,
$eventEnvelope->event->type->value,
json_encode($eventData['originDimensionSpacePoint']),
json_encode($eventData['affectedDimensionSpacePoints'])
]
);
$this->updateEvent($eventEnvelope->sequenceNumber, $eventData);
}
}
}

$outputFn('Rewriting completed. Now catching up the GraphProjection to final state.');
$contentGraphProjection->catchUp($eventStream, $this->contentRepository);

if ($this->projections->has(DocumentUriPathProjection::class)) {
$outputFn('Found DocumentUriPathProjection. Will replay this, as it relies on the updated affectedDimensionSpacePoints');
$documentUriPathProjection = $this->projections->get(DocumentUriPathProjection::class);
$documentUriPathProjection->reset();
$documentUriPathProjection->catchUp($eventStream, $this->contentRepository);
}

$outputFn('All done.');
}


private function updateEvent(SequenceNumber $sequenceNumber, array $eventData)
{
$eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId);
$this->connection->beginTransaction();
$this->connection->executeStatement(
'UPDATE ' . $eventTableName . ' SET payload=:payload WHERE sequencenumber=:sequenceNumber',
[
'payload' => json_encode($eventData),
'sequenceNumber' => $sequenceNumber->value
]
);
$this->connection->commit();
}

private function copyEventTable(string $backupEventTableName)
{
$eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId);
$this->connection->executeStatement(
'CREATE TABLE ' . $backupEventTableName . ' AS
SELECT *
FROM ' . $eventTableName
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);

namespace Neos\ContentRepositoryRegistry\Service;

use Doctrine\DBAL\Connection;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepositoryRegistry\Command\MigrateEventsCommandController;
use Neos\EventStore\DoctrineAdapter\DoctrineEventStore;
use Neos\Flow\Annotations as Flow;

/**
* Factory for the {@see EventMigrationService}
*
* @implements ContentRepositoryServiceFactoryInterface<EventMigrationService>
* @internal this is currently only used by the {@see MigrateEventsCommandController}
*/
#[Flow\Scope("singleton")]
final class EventMigrationServiceFactory implements ContentRepositoryServiceFactoryInterface
{
public function __construct(
private readonly Connection $connection,
) {}

public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface
{
if (!($serviceFactoryDependencies->eventStore instanceof DoctrineEventStore)) {
throw new \RuntimeException('EventMigrationService only works with DoctrineEventStore, ' . get_class($serviceFactoryDependencies->eventStore) . ' given');
}

return new EventMigrationService(
$serviceFactoryDependencies->projections,
$serviceFactoryDependencies->contentRepositoryId,
$serviceFactoryDependencies->contentRepository,
$serviceFactoryDependencies->eventStore,
$this->connection
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -539,57 +539,59 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn
// TODO Can there be more affected dimension space points and how to determine them?
// see https://github.com/neos/contentrepository-development-collection/issues/163

$node = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash(
$event->nodeAggregateId,
$event->originDimensionSpacePoint->hash
));

if ($node === null) {
// probably not a document node
return;
}
if ((isset($newPropertyValues['targetMode']) || isset($newPropertyValues['target'])) && $node->isShortcut()) {
$shortcutTarget = $node->getShortcutTarget();
$shortcutTarget = [
'mode' => $newPropertyValues['targetMode'] ?? $shortcutTarget['mode'],
'target' => $newPropertyValues['target'] ?? $shortcutTarget['target'],
];
$this->updateNodeByIdAndDimensionSpacePointHash(
foreach ($event->affectedDimensionSpacePoints as $affectedDimensionSpacePoint) {
$node = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash(
$event->nodeAggregateId,
$event->originDimensionSpacePoint->hash,
['shortcutTarget' => $shortcutTarget]
);
}
$affectedDimensionSpacePoint->hash
));

if (!isset($newPropertyValues['uriPathSegment'])) {
return;
}
$oldUriPath = $node->getUriPath();
// homepage -> TODO hacky?
if ($oldUriPath === '') {
return;
}
/** @var string[] $uriPathSegments */
$uriPathSegments = explode('/', $oldUriPath);
$uriPathSegments[array_key_last($uriPathSegments)] = $newPropertyValues['uriPathSegment'];
$newUriPath = implode('/', $uriPathSegments);
if ($node === null) {
// probably not a document node
continue;
}
if ((isset($newPropertyValues['targetMode']) || isset($newPropertyValues['target'])) && $node->isShortcut()) {
$shortcutTarget = $node->getShortcutTarget();
$shortcutTarget = [
'mode' => $newPropertyValues['targetMode'] ?? $shortcutTarget['mode'],
'target' => $newPropertyValues['target'] ?? $shortcutTarget['target'],
];
$this->updateNodeByIdAndDimensionSpacePointHash(
$event->nodeAggregateId,
$affectedDimensionSpacePoint->hash,
['shortcutTarget' => $shortcutTarget]
);
}

$this->updateNodeQuery(
'SET uriPath = CONCAT(:newUriPath, SUBSTRING(uriPath, LENGTH(:oldUriPath) + 1))
if (!isset($newPropertyValues['uriPathSegment'])) {
continue;
}
$oldUriPath = $node->getUriPath();
// homepage -> TODO hacky?
if ($oldUriPath === '') {
continue;
}
/** @var string[] $uriPathSegments */
$uriPathSegments = explode('/', $oldUriPath);
$uriPathSegments[array_key_last($uriPathSegments)] = $newPropertyValues['uriPathSegment'];
$newUriPath = implode('/', $uriPathSegments);

$this->updateNodeQuery(
'SET uriPath = CONCAT(:newUriPath, SUBSTRING(uriPath, LENGTH(:oldUriPath) + 1))
WHERE dimensionSpacePointHash = :dimensionSpacePointHash
AND (
nodeAggregateId = :nodeAggregateId
OR nodeAggregateIdPath LIKE :childNodeAggregateIdPathPrefix
)',
[
'newUriPath' => $newUriPath,
'oldUriPath' => $oldUriPath,
'dimensionSpacePointHash' => $event->originDimensionSpacePoint->hash,
'nodeAggregateId' => $node->getNodeAggregateId()->value,
'childNodeAggregateIdPathPrefix' => $node->getNodeAggregateIdPath() . '/%',
]
);
$this->emitDocumentUriPathChanged($oldUriPath, $newUriPath, $event, $eventEnvelope);
[
'newUriPath' => $newUriPath,
'oldUriPath' => $oldUriPath,
'dimensionSpacePointHash' => $affectedDimensionSpacePoint->hash,
'nodeAggregateId' => $node->getNodeAggregateId()->value,
'childNodeAggregateIdPathPrefix' => $node->getNodeAggregateIdPath() . '/%',
]
);
$this->emitDocumentUriPathChanged($oldUriPath, $newUriPath, $event, $eventEnvelope);
}
}

private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void
Expand Down

0 comments on commit 782b958

Please sign in to comment.