Skip to content

Commit

Permalink
Merge pull request #5330 from dlubitz/90/feature/root-node-type-mapping
Browse files Browse the repository at this point in the history
FEATURE: Allow mapping of legacy root paths to RootNodeAggregate  NodeTypeNames
  • Loading branch information
dlubitz authored Nov 5, 2024
2 parents 51d5e4e + 1266a82 commit 1b6285b
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -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
Expand All @@ -68,13 +70,15 @@ 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) {
throw new \InvalidArgumentException(sprintf('Failed to get database connection, check the --config parameter: %s', $e->getMessage()), 1659527201, $e);
}
} 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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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());
}

Expand Down Expand Up @@ -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]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function __construct(
private readonly PropertyConverter $propertyConverter,
private readonly EventStoreInterface $eventStore,
private readonly ContentStreamId $contentStreamId,
private readonly RootNodeTypeMapping $rootNodeTypeMapping
) {
}

Expand All @@ -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),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function __construct(
private readonly ResourceManager $resourceManager,
private readonly PropertyMapper $propertyMapper,
private readonly ContentStreamId $contentStreamId,
private readonly RootNodeTypeMapping $rootNodeTypeMapping,
) {
}

Expand All @@ -63,6 +64,7 @@ public function build(
$serviceFactoryDependencies->propertyConverter,
$serviceFactoryDependencies->eventStore,
$this->contentStreamId,
$this->rootNodeTypeMapping,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -545,7 +535,7 @@ private function isNodeHidden(array $nodeDataRow): bool
&& (
$hiddenBeforeDateTime == null
|| $hiddenBeforeDateTime > $now
|| $hiddenBeforeDateTime<= $hiddenAfterDateTime
|| $hiddenBeforeDateTime <= $hiddenAfterDateTime
)
) {
return true;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace Neos\ContentRepository\LegacyNodeMigration;

use Neos\ContentRepository\Core\NodeType\NodeTypeName;

class RootNodeTypeMapping
{
/**
* @param array<string, string> $mapping
*/
private function __construct(
public readonly array $mapping,
) {
}

/**
* @param array<string, string> $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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Neos:
ContentRepository:
LegacyNodeMigration:
rootNodeMapping:
default:
'/sites': 'Neos.Neos:Sites'
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}}} |

0 comments on commit 1b6285b

Please sign in to comment.