Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TASK: Workspace aware commands events migration #4890

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Feature: Constraint checks on SetNodeReferences
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" with code 1521386692
Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" with code 1710407870

# checks for sourceNodeAggregateId
Scenario: Try to reference nodes in a non-existent node aggregate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,10 @@ protected function requireContentStream(
WorkspaceName $workspaceName,
ContentRepository $contentRepository
): ContentStreamId {
$contentStreamId = ContentStreamIdOverride::$contentStreamIdToUse
?: $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName)?->currentContentStreamId;
if (!$contentStreamId || !$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) {
$contentStreamId = ContentStreamIdOverride::findContentStreamIdForWorkspace($contentRepository, $workspaceName);
if (!$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) {
throw new ContentStreamDoesNotExistYet(
'Content stream "' . $contentStreamId?->value . '" does not exist yet.',
'Content stream "' . $contentStreamId->value . '" does not exist yet.',
1521386692
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

namespace Neos\ContentRepository\Core\Feature\Common;

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;

/**
* @internal (slightly hacky) implementation details for the workspace command handler
Expand All @@ -24,13 +27,41 @@ class ContentStreamIdOverride
/**
* A content stream id that to use instead of the workspace one's {@see ConstraintChecks::requireContentStream()}
*/
public static ?ContentStreamId $contentStreamIdToUse = null;
private static ?ContentStreamId $contentStreamIdToUse = null;

/**
* @internal
*/
public static function useContentStreamId(?ContentStreamId $contentStreamIdToUse): void
public static function withContentStreamIdToUse(ContentStreamId $contentStreamIdToUse, \Closure $fn): void
ahaeslich marked this conversation as resolved.
Show resolved Hide resolved
{
if (self::$contentStreamIdToUse !== null) {
throw new \Exception('Recursive content stream override is not supported', 1710426945);
}
self::$contentStreamIdToUse = $contentStreamIdToUse;
try {
$fn();
} catch (\Throwable $th) {
self::$contentStreamIdToUse = null;
throw $th;
}
nezaniel marked this conversation as resolved.
Show resolved Hide resolved
self::$contentStreamIdToUse = null;
ahaeslich marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @internal
*/
public static function findContentStreamIdForWorkspace(ContentRepository $contentRepository, WorkspaceName $workspaceName): ContentStreamId
ahaeslich marked this conversation as resolved.
Show resolved Hide resolved
{
$contentStreamId = self::$contentStreamIdToUse
?: $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName)?->currentContentStreamId;

if (!$contentStreamId) {
throw new ContentStreamDoesNotExistYet(
'Content stream for workspace "' . $workspaceName->value . '" does not exist yet.',
1710407870
);
}

return $contentStreamId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ private function handleRebaseWorkspace(
// - extract the commands from the to-be-rebased content stream; and applies them on the new content stream
$originalCommands = $this->extractCommandsFromContentStreamMetadata($workspaceContentStreamName);
$rebaseStatistics = new WorkspaceRebaseStatistics();
$this->withContentStreamIdToUse(
ContentStreamIdOverride::withContentStreamIdToUse(
$command->rebasedContentStreamId,
function () use ($originalCommands, $contentRepository, $rebaseStatistics, $workspaceContentStreamName, $baseWorkspace): void {
foreach ($originalCommands as $i => $originalCommand) {
Expand Down Expand Up @@ -531,7 +531,7 @@ private function handlePublishIndividualNodesFromWorkspace(

try {
// 4) using the new content stream, apply the matching commands
$this->withContentStreamIdToUse(
ContentStreamIdOverride::withContentStreamIdToUse(
$command->contentStreamIdForMatchingPart,
function () use ($matchingCommands, $contentRepository, $baseWorkspace, $command): void {
foreach ($matchingCommands as $matchingCommand) {
Expand Down Expand Up @@ -565,7 +565,7 @@ function () use ($matchingCommands, $contentRepository, $baseWorkspace, $command
)->block();

// 7) apply REMAINING commands to the workspace's new content stream
$this->withContentStreamIdToUse(
ContentStreamIdOverride::withContentStreamIdToUse(
$command->contentStreamIdForRemainingPart,
function () use ($contentRepository, $remainingCommands) {
foreach ($remainingCommands as $remainingCommand) {
Expand Down Expand Up @@ -668,7 +668,7 @@ private function handleDiscardIndividualNodesFromWorkspace(

// 4) using the new content stream, apply the commands to keep
try {
$this->withContentStreamIdToUse(
ContentStreamIdOverride::withContentStreamIdToUse(
$command->newContentStreamId,
function () use ($commandsToKeep, $contentRepository, $baseWorkspace, $command): void {
foreach ($commandsToKeep as $matchingCommand) {
Expand Down Expand Up @@ -990,19 +990,4 @@ private function hasEventsInContentStreamExceptForking(

return false;
}

private function withContentStreamIdToUse(ContentStreamId $contentStreamIdToUse, callable $function): void
{
if (ContentStreamIdOverride::$contentStreamIdToUse !== null) {
throw new \Exception('Recursive content stream override is not supported');
}
ContentStreamIdOverride::useContentStreamId($contentStreamIdToUse);
try {
$function();
} catch (\Exception $exception) {
ContentStreamIdOverride::useContentStreamId(null);
throw $exception;
}
ContentStreamIdOverride::useContentStreamId(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct(
*
* Needed for #4322: https://github.com/neos/neos-development-collection/pull/4322
*
* Included in February 2023 - before final Neos 9.0 release
* Included in February 2024 - before final Neos 9.0 release
*
* @param string $contentRepository Identifier of the Content Repository to migrate
*/
Expand All @@ -32,4 +32,20 @@ public function migratePropertiesToUnsetCommand(string $contentRepository = 'def
$eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory);
$eventMigrationService->migratePropertiesToUnset($this->outputLine(...));
}

/**
* Adds a dummy workspace name to the events meta-data, so it can be rebased
*
* Needed for #4708: https://github.com/neos/neos-development-collection/pull/4708
*
* Included in March 2024 - before final Neos 9.0 release
*
* @param string $contentRepository Identifier of the Content Repository to migrate
*/
public function migrateMetaDataToWorkspaceNameCommand(string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory);
$eventMigrationService->migrateMetaDataToWorkspaceName($this->outputLine(...));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
namespace Neos\ContentRepositoryRegistry\Service;

use Doctrine\DBAL\Connection;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface;
use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties;
use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively;
use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties;
use Neos\ContentRepository\Core\Projection\CatchUpOptions;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjection;
use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences;
use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName;
use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType;
use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant;
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode;
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions;
use Neos\ContentRepositoryRegistry\Command\MigrateEventsCommandController;
use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory;
use Neos\EventStore\EventStoreInterface;
Expand Down Expand Up @@ -70,7 +78,7 @@ public function __construct(
*
* Needed for #4322: https://github.com/neos/neos-development-collection/pull/4322
*
* Included in February 2023 - before final Neos 9.0 release
* Included in February 2024 - before final Neos 9.0 release
*
* @param \Closure $outputFn
* @return void
Expand Down Expand Up @@ -222,6 +230,88 @@ public function migratePropertiesToUnset(\Closure $outputFn)
}
}


/**
* Adds a dummy workspace name to the events meta-data, so it can be rebased
*
* The value of the payload for `workspaceName` is only required to successfully instantiate a command by its metadata.
* This is only necessary for rebasing where directly override the workspace name to the target one.
* Thus, we simply enter a dummy string "missing:{contentStreamId}".
*
* Needed for #4708: https://github.com/neos/neos-development-collection/pull/4708
*
* Included in March 2024 - before final Neos 9.0 release
*
* @param \Closure $outputFn
* @return void
*/
public function migrateMetaDataToWorkspaceName(\Closure $outputFn)
{
$this->eventsModified = [];

$backupEventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId)
. '_bak_' . date('Y_m_d_H_i_s');
ahaeslich marked this conversation as resolved.
Show resolved Hide resolved
$outputFn('Backup: copying events table to %s', [$backupEventTableName]);

$this->copyEventTable($backupEventTableName);

$streamName = VirtualStreamName::all();
$eventStream = $this->eventStore->load($streamName);
foreach ($eventStream as $eventEnvelope) {
$outputRewriteNotice = fn(string $message) => $outputFn(sprintf('%s@%s %s', $eventEnvelope->sequenceNumber->value, $eventEnvelope->event->type->value, $message));
$eventMetaData = $eventEnvelope->event->metadata?->value;

if (!$eventMetaData || !($commandClassName = $eventMetaData['commandClass'] ?? null)) {
continue;
}

/**
* Nearly all implementations of {@see RebasableToOtherWorkspaceInterface::createCopyForWorkspace()} have to migrate.
* The following commands all require the `$workspaceName` field and have no `$contentStreamId`.
* The commands AddDimensionShineThrough and MoveDimensionSpacePoint are exceptions to the rule, which don't
* require workspaces but still operate on content streams. {@link https://github.com/neos/neos-development-collection/issues/4942}
*/
if (!in_array($commandClassName, [
CreateNodeAggregateWithNodeAndSerializedProperties::class,
DisableNodeAggregate::class,
EnableNodeAggregate::class,
CopyNodesRecursively::class,
SetSerializedNodeProperties::class,
MoveNodeAggregate::class,
SetSerializedNodeReferences::class,
RemoveNodeAggregate::class,
ChangeNodeAggregateName::class,
ChangeNodeAggregateType::class,
CreateNodeVariant::class,
CreateRootNodeAggregateWithNode::class,
UpdateRootNodeAggregateDimensions::class,
])) {
continue;
}

if (isset($eventMetaData['commandPayload']['workspaceName'])) {
continue;
}

// ... and update the event
// the payload is only used for rebasing where we override the workspace either way:
$eventMetaData['commandPayload']['workspaceName'] = 'missing:' . ($eventMetaData['commandPayload']['contentStreamId'] ?? '');
unset($eventMetaData['commandPayload']['contentStreamId']);

$outputRewriteNotice(sprintf('Metadata: Added `workspaceName`'));

$this->updateEventMetaData($eventEnvelope->sequenceNumber, $eventMetaData);
}

if (!count($this->eventsModified)) {
$outputFn('Migration was not necessary.');
return;
}

$outputFn();
$outputFn(sprintf('Migration applied to %s events.', count($this->eventsModified)));
}

/**
* @param array<mixed> $payload
*/
Expand Down
Loading