diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index 337a3aa66c..249908cc68 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationService; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationServiceFactory; +use Neos\ContentRepository\LegacyNodeMigration\RootNodeTypeMapping; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; @@ -34,6 +35,7 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; class CrCommandController extends CommandController { @@ -56,7 +58,7 @@ public function __construct( * Migrate from the Legacy CR * * @param bool $verbose If set, all notices will be rendered - * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path"}' + * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' * @throws \Exception */ public function migrateLegacyDataCommand(bool $verbose = false, string $config = null): void @@ -68,6 +70,7 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = throw new \InvalidArgumentException(sprintf('Failed to parse --config parameter: %s', $e->getMessage()), 1659526855, $e); } $resourcesPath = $parsedConfig['resourcesPath'] ?? self::defaultResourcesPath(); + $rootNodes = isset($parsedConfig['rootNodes']) ? RootNodeTypeMapping::fromArray($parsedConfig['rootNodes']) : $this->getDefaultRootNodes(); try { $connection = isset($parsedConfig['dbal']) ? DriverManager::getConnection(array_merge($this->connection->getParams(), $parsedConfig['dbal']), new Configuration()) : $this->connection; } catch (DBALException $e) { @@ -75,6 +78,7 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = } } else { $resourcesPath = $this->determineResourcesPath(); + $rootNodes = $this->getDefaultRootNodes(); if (!$this->output->askConfirmation(sprintf('Do you want to migrate nodes from the current database "%s@%s" (y/n)? ', $this->connection->getParams()['dbname'] ?? '?', $this->connection->getParams()['host'] ?? '?'))) { $connection = $this->adjustDataBaseConnection($this->connection); } else { @@ -137,7 +141,8 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = $this->resourceRepository, $this->resourceManager, $this->propertyMapper, - $liveContentStreamId + $liveContentStreamId, + $rootNodes, ) ); assert($legacyMigrationService instanceof LegacyMigrationService); @@ -158,14 +163,14 @@ private function adjustDataBaseConnection(Connection $connection): Connection { $connectionParams = $connection->getParams(); $connectionParams['driver'] = $this->output->select(sprintf('Driver? [%s] ', $connectionParams['driver'] ?? ''), ['pdo_mysql', 'pdo_sqlite', 'pdo_pgsql'], $connectionParams['driver'] ?? null); - $connectionParams['host'] = $this->output->ask(sprintf('Host? [%s] ',$connectionParams['host'] ?? ''), $connectionParams['host'] ?? null); - $port = $this->output->ask(sprintf('Port? [%s] ',$connectionParams['port'] ?? ''), isset($connectionParams['port']) ? (string)$connectionParams['port'] : null); + $connectionParams['host'] = $this->output->ask(sprintf('Host? [%s] ', $connectionParams['host'] ?? ''), $connectionParams['host'] ?? null); + $port = $this->output->ask(sprintf('Port? [%s] ', $connectionParams['port'] ?? ''), isset($connectionParams['port']) ? (string)$connectionParams['port'] : null); $connectionParams['port'] = isset($port) ? (int)$port : null; - $connectionParams['dbname'] = $this->output->ask(sprintf('DB name? [%s] ',$connectionParams['dbname'] ?? ''), $connectionParams['dbname'] ?? null); - $connectionParams['user'] = $this->output->ask(sprintf('DB user? [%s] ',$connectionParams['user'] ?? ''), $connectionParams['user'] ?? null); + $connectionParams['dbname'] = $this->output->ask(sprintf('DB name? [%s] ', $connectionParams['dbname'] ?? ''), $connectionParams['dbname'] ?? null); + $connectionParams['user'] = $this->output->ask(sprintf('DB user? [%s] ', $connectionParams['user'] ?? ''), $connectionParams['user'] ?? null); /** @phpstan-ignore-next-line */ $connectionParams['password'] = $this->output->askHiddenResponse(sprintf('DB password? [%s]', str_repeat('*', strlen($connectionParams['password'] ?? '')))) ?? $connectionParams['password']; - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line */ return DriverManager::getConnection($connectionParams, new Configuration()); } @@ -202,4 +207,9 @@ private static function defaultResourcesPath(): string { return FLOW_PATH_DATA . 'Persistent/Resources'; } + + private function getDefaultRootNodes(): RootNodeTypeMapping + { + return RootNodeTypeMapping::fromArray(['/sites' => NodeTypeNameFactory::NAME_SITES]); + } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php index e95c5b27dc..f2ef4113a0 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php @@ -58,6 +58,7 @@ public function __construct( private readonly PropertyConverter $propertyConverter, private readonly EventStoreInterface $eventStore, private readonly ContentStreamId $contentStreamId, + private readonly RootNodeTypeMapping $rootNodeTypeMapping ) { } @@ -73,7 +74,7 @@ public function runAllProcessors(\Closure $outputLineFn, bool $verbose = false): /** @var ProcessorInterface[] $processors */ $processors = [ 'Exporting assets' => new NodeDataToAssetsProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), - 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, $filesystem, new NodeDataLoader($this->connection)), + 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, $filesystem, $this->rootNodeTypeMapping,new NodeDataLoader($this->connection)), 'Importing assets' => new AssetRepositoryImportProcessor($filesystem, $this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), 'Importing events' => new EventStoreImportProcessor(true, $filesystem, $this->eventStore, $this->eventNormalizer, $this->contentStreamId), ]; diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php index 67bd7df05c..2010bc989e 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php @@ -41,6 +41,7 @@ public function __construct( private readonly ResourceManager $resourceManager, private readonly PropertyMapper $propertyMapper, private readonly ContentStreamId $contentStreamId, + private readonly RootNodeTypeMapping $rootNodeTypeMapping, ) { } @@ -63,6 +64,7 @@ public function build( $serviceFactoryDependencies->propertyConverter, $serviceFactoryDependencies->eventStore, $this->contentStreamId, + $this->rootNodeTypeMapping, ); } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index 894cfa441e..4358241bcc 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -62,7 +62,6 @@ final class NodeDataToEventsProcessor implements ProcessorInterface * @var array<\Closure> */ private array $callbacks = []; - private NodeTypeName $sitesNodeTypeName; private WorkspaceName $workspaceName; private ContentStreamId $contentStreamId; private VisitedNodeAggregates $visitedNodes; @@ -91,9 +90,9 @@ public function __construct( private readonly InterDimensionalVariationGraph $interDimensionalVariationGraph, private readonly EventNormalizer $eventNormalizer, private readonly Filesystem $files, + private readonly RootNodeTypeMapping $rootNodeTypeMapping, private readonly iterable $nodeDataRows, ) { - $this->sitesNodeTypeName = NodeTypeNameFactory::forSites(); $this->contentStreamId = ContentStreamId::create(); $this->workspaceName = WorkspaceName::forLive(); $this->visitedNodes = new VisitedNodeAggregates(); @@ -104,18 +103,6 @@ public function setContentStreamId(ContentStreamId $contentStreamId): void $this->contentStreamId = $contentStreamId; } - public function setSitesNodeType(NodeTypeName $nodeTypeName): void - { - $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - if (!$nodeType?->isOfType(NodeTypeNameFactory::NAME_SITES)) { - throw new \InvalidArgumentException( - sprintf('Sites NodeType "%s" must be of type "%s"', $nodeTypeName->value, NodeTypeNameFactory::NAME_SITES), - 1695802415 - ); - } - $this->sitesNodeTypeName = $nodeTypeName; - } - public function onMessage(\Closure $callback): void { $this->callbacks[] = $callback; @@ -126,11 +113,14 @@ public function run(): ProcessorResult $this->resetRuntimeState(); foreach ($this->nodeDataRows as $nodeDataRow) { - if ($nodeDataRow['path'] === '/sites') { - $sitesNodeAggregateId = NodeAggregateId::fromString($nodeDataRow['identifier']); - $this->visitedNodes->addRootNode($sitesNodeAggregateId, $this->sitesNodeTypeName, NodePath::fromString('/sites'), $this->interDimensionalVariationGraph->getDimensionSpacePoints()); - $this->exportEvent(new RootNodeAggregateWithNodeWasCreated($this->workspaceName, $this->contentStreamId, $sitesNodeAggregateId, $this->sitesNodeTypeName, $this->interDimensionalVariationGraph->getDimensionSpacePoints(), NodeAggregateClassification::CLASSIFICATION_ROOT)); - continue; + if ($this->isRootNodePath($nodeDataRow['path'])) { + $rootNodeTypeName = $this->rootNodeTypeMapping->getByPath($nodeDataRow['path']); + if ($rootNodeTypeName) { + $rootNodeAggregateId = NodeAggregateId::fromString($nodeDataRow['identifier']); + $this->visitedNodes->addRootNode($rootNodeAggregateId, $rootNodeTypeName, NodePath::fromString($nodeDataRow['path']), $this->interDimensionalVariationGraph->getDimensionSpacePoints()); + $this->exportEvent(new RootNodeAggregateWithNodeWasCreated($this->workspaceName, $this->contentStreamId, $rootNodeAggregateId, $rootNodeTypeName, $this->interDimensionalVariationGraph->getDimensionSpacePoints(), NodeAggregateClassification::CLASSIFICATION_ROOT)); + continue; + } } if ($this->metaDataExported === false && $nodeDataRow['parentpath'] === '/sites') { $this->exportMetaData($nodeDataRow); @@ -545,7 +535,7 @@ private function isNodeHidden(array $nodeDataRow): bool && ( $hiddenBeforeDateTime == null || $hiddenBeforeDateTime > $now - || $hiddenBeforeDateTime<= $hiddenAfterDateTime + || $hiddenBeforeDateTime <= $hiddenAfterDateTime ) ) { return true; @@ -565,4 +555,9 @@ private function isNodeHidden(array $nodeDataRow): bool return false; } + + private function isRootNodePath(string $path): bool + { + return strpos($path, '/') === 0 && strpos($path, '/', 1) === false; + } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/RootNodeTypeMapping.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/RootNodeTypeMapping.php new file mode 100644 index 0000000000..5b3bf28910 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/RootNodeTypeMapping.php @@ -0,0 +1,31 @@ + $mapping + */ + private function __construct( + public readonly array $mapping, + ) { + } + + /** + * @param array $mapping + * @return self + */ + public static function fromArray(array $mapping): self + { + return new self($mapping); + } + + public function getByPath(string $path): ?NodeTypeName + { + return isset($this->mapping[$path]) ? NodeTypeName::fromString($this->mapping[$path]) : null; + } +} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Configuration/Settings.yaml b/Neos.ContentRepository.LegacyNodeMigration/Configuration/Settings.yaml new file mode 100644 index 0000000000..456ed3f206 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Configuration/Settings.yaml @@ -0,0 +1,6 @@ +Neos: + ContentRepository: + LegacyNodeMigration: + rootNodeMapping: + default: + '/sites': 'Neos.Neos:Sites' diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 7ab7f5a37a..d73bf80096 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -32,10 +32,12 @@ use Neos\ContentRepository\Export\Severity; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToAssetsProcessor; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToEventsProcessor; +use Neos\ContentRepository\LegacyNodeMigration\RootNodeTypeMapping; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Property\PropertyMapper; use Neos\Flow\ResourceManagement\PersistentResource; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use PHPUnit\Framework\Assert; use PHPUnit\Framework\MockObject\Generator as MockGenerator; @@ -106,18 +108,28 @@ public function iHaveTheFollowingNodeDataRows(TableNode $nodeDataRows): void 'properties' => !empty($row['Properties']) ? $row['Properties'] : '{}', 'dimensionvalues' => !empty($row['Dimension Values']) ? $row['Dimension Values'] : '{}', 'hiddeninindex' => $row['Hidden in index'] ?? '0', - 'hiddenbeforedatetime' => !empty($row['Hidden before DateTime']) ? ($row['Hidden before DateTime']): null, - 'hiddenafterdatetime' => !empty($row['Hidden after DateTime']) ? ($row['Hidden after DateTime']) : null, + 'hiddenbeforedatetime' => !empty($row['Hidden before DateTime']) ? ($row['Hidden before DateTime']) : null, + 'hiddenafterdatetime' => !empty($row['Hidden after DateTime']) ? ($row['Hidden after DateTime']) : null, 'hidden' => $row['Hidden'] ?? '0', ]; }, $nodeDataRows->getHash()); } + /** + * @When /^I run the event migration for content stream (.*) with rootNode mapping (.*)$/ + */ + public function iRunTheEventMigrationForContentStreamWithRootnodeMapping(string $contentStream = null, string $rootNodeMapping): void + { + $contentStream = trim($contentStream, '"'); + $rootNodeTypeMapping = RootNodeTypeMapping::fromArray(json_decode($rootNodeMapping, true)); + $this->iRunTheEventMigration($contentStream, $rootNodeTypeMapping); + } + /** * @When I run the event migration * @When I run the event migration for content stream :contentStream */ - public function iRunTheEventMigration(string $contentStream = null): void + public function iRunTheEventMigration(string $contentStream = null, RootNodeTypeMapping $rootNodeTypeMapping = null): void { $nodeTypeManager = $this->currentContentRepository->getNodeTypeManager(); $propertyMapper = $this->getObject(PropertyMapper::class); @@ -142,6 +154,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $this->currentContentRepository->getVariationGraph(), $this->getObject(EventNormalizer::class), $this->mockFilesystem, + $rootNodeTypeMapping ?? RootNodeTypeMapping::fromArray(['/sites' => NodeTypeNameFactory::NAME_SITES]), $this->nodeDataRows ); if ($contentStream !== null) { diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature new file mode 100644 index 0000000000..2834b27a23 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature @@ -0,0 +1,46 @@ +@contentrepository +Feature: Simple migrations without content dimensions but other root nodetype name + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true + properties: + 'text': + type: string + defaultValue: 'My default text' + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + + Scenario: Migration without rootNodeType configuration for all root nodes + When I have the following node data rows: + | Identifier | Path | Node Type | Properties | + | sites-node-id | /sites | unstructured | | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | + | test-root-node-id | /test | unstructured | | + | test-node-id | /test/test-site | Some.Package:Homepage | {"text": "foo"} | + And I run the event migration for content stream "cs-id" + Then I expect the following errors to be logged + | Failed to find parent node for node with id "test-root-node-id" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | + | Failed to find parent node for node with id "test-node-id" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | + + + Scenario: Migration with rootNodeType configuration for all root nodes + When I have the following node data rows: + | Identifier | Path | Node Type | Properties | + | sites-node-id | /sites | unstructured | | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | + | test-root-node-id | /test | unstructured | | + | test-node-id | /test/test-site | Some.Package:Homepage | {"text": "foo"} | + And I run the event migration for content stream "cs-id" with rootNode mapping {"/sites": "Neos.Neos:Sites", "/test": "Neos.ContentRepository.LegacyNodeMigration:TestRoot"} + Then I expect the following events to be exported + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "test-root-node-id", "nodeTypeName": "Neos.ContentRepository.LegacyNodeMigration:TestRoot", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "test-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "test-root-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} |