diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a157ca8538..2ab48800296 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -244,6 +244,7 @@ jobs: touch ${{ github.workspace }}/Data/DebugDatabaseDumps/keep ./flow package:list --loading-order + FLOW_CONTEXT=Testing/Behat ./flow doctrine:migrate --quiet cd Packages/Neos # composer test:behavioral diff --git a/Neos.ContentRepository.Export/src/ExportService.php b/Neos.ContentRepository.Export/src/ExportService.php index 00a45bcbb16..97dcff6bc87 100644 --- a/Neos.ContentRepository.Export/src/ExportService.php +++ b/Neos.ContentRepository.Export/src/ExportService.php @@ -6,11 +6,12 @@ use League\Flysystem\Filesystem; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Export\Processors\AssetExportProcessor; use Neos\ContentRepository\Export\Processors\EventExportProcessor; use Neos\EventStore\EventStoreInterface; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; +use Neos\Neos\AssetUsage\AssetUsageService; /** * @internal @@ -19,10 +20,11 @@ class ExportService implements ContentRepositoryServiceInterface { public function __construct( + private readonly ContentRepositoryId $contentRepositoryId, private readonly Filesystem $filesystem, private readonly WorkspaceFinder $workspaceFinder, private readonly AssetRepository $assetRepository, - private readonly AssetUsageFinder $assetUsageFinder, + private readonly AssetUsageService $assetUsageService, private readonly EventStoreInterface $eventStore, ) { } @@ -37,10 +39,11 @@ public function runAllProcessors(\Closure $outputLineFn, bool $verbose = false): $this->eventStore ), 'Exporting assets' => new AssetExportProcessor( + $this->contentRepositoryId, $this->filesystem, $this->assetRepository, $this->workspaceFinder, - $this->assetUsageFinder + $this->assetUsageService ) ]; diff --git a/Neos.ContentRepository.Export/src/ExportServiceFactory.php b/Neos.ContentRepository.Export/src/ExportServiceFactory.php index baa013bca98..d93b47c2d40 100644 --- a/Neos.ContentRepository.Export/src/ExportServiceFactory.php +++ b/Neos.ContentRepository.Export/src/ExportServiceFactory.php @@ -8,7 +8,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; +use Neos\Neos\AssetUsage\AssetUsageService; /** * @internal @@ -21,17 +21,18 @@ public function __construct( private readonly Filesystem $filesystem, private readonly WorkspaceFinder $workspaceFinder, private readonly AssetRepository $assetRepository, - private readonly AssetUsageFinder $assetUsageFinder, + private readonly AssetUsageService $assetUsageService, ) { } public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ExportService { return new ExportService( + $serviceFactoryDependencies->contentRepositoryId, $this->filesystem, $this->workspaceFinder, $this->assetRepository, - $this->assetUsageFinder, + $this->assetUsageService, $serviceFactoryDependencies->eventStore, ); } diff --git a/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php b/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php index db7679a18b9..ff2fa71dab9 100644 --- a/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php @@ -4,6 +4,7 @@ use League\Flysystem\Filesystem; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; @@ -15,8 +16,8 @@ use Neos\Media\Domain\Model\AssetVariantInterface; use Neos\Media\Domain\Model\ImageVariant; use Neos\Media\Domain\Repository\AssetRepository; +use Neos\Neos\AssetUsage\AssetUsageService; use Neos\Neos\AssetUsage\Dto\AssetUsageFilter; -use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; /** * Processor that exports all assets and resources used in the Neos live workspace to the file system @@ -29,10 +30,11 @@ final class AssetExportProcessor implements ProcessorInterface private array $callbacks = []; public function __construct( + private readonly ContentRepositoryId $contentRepositoryId, private readonly Filesystem $files, private readonly AssetRepository $assetRepository, private readonly WorkspaceFinder $workspaceFinder, - private readonly AssetUsageFinder $assetUsageFinder, + private readonly AssetUsageService $assetUsageService, ) {} public function onMessage(\Closure $callback): void @@ -47,13 +49,13 @@ public function run(): ProcessorResult if ($liveWorkspace === null) { return ProcessorResult::error('Failed to find live workspace'); } - $assetFilter = AssetUsageFilter::create()->withContentStream($liveWorkspace->currentContentStreamId)->groupByAsset(); + $assetFilter = AssetUsageFilter::create()->withWorkspaceName($liveWorkspace->workspaceName)->groupByAsset(); $numberOfExportedAssets = 0; $numberOfExportedImageVariants = 0; $numberOfErrors = 0; - foreach ($this->assetUsageFinder->findByFilter($assetFilter) as $assetUsage) { + foreach ($this->assetUsageService->findByFilter($this->contentRepositoryId, $assetFilter) as $assetUsage) { /** @var Asset|null $asset */ $asset = $this->assetRepository->findByIdentifier($assetUsage->assetId); if ($asset === null) { diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index 17baf481e21..5861c9e77c2 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -106,7 +106,7 @@ public function relatedNodesAction(AssetInterface $asset) $contentRepository = $this->contentRepositoryRegistry->get($usage->getContentRepositoryId()); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->getContentStreamId()); + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($usage->getWorkspaceName()); // FIXME: AssetUsageReference->workspaceName ? $nodeAggregate = $contentRepository->getContentGraph($workspace->workspaceName)->findNodeAggregateById( diff --git a/Neos.Media.Browser/Resources/Private/Templates/Usage/RelatedNodes.html b/Neos.Media.Browser/Resources/Private/Templates/Usage/RelatedNodes.html index 921537fb5c8..2a1b2c27af1 100644 --- a/Neos.Media.Browser/Resources/Private/Templates/Usage/RelatedNodes.html +++ b/Neos.Media.Browser/Resources/Private/Templates/Usage/RelatedNodes.html @@ -36,23 +36,21 @@ - {neos:backend.translate(id: 'workspaces.personalWorkspace', source: 'Modules', package: 'Neos.Neos')} + {neos:backend.translate(id: 'workspaces.personalWorkspace', source: 'Main', package: 'Neos.Workspace.Ui')} - {neos:backend.translate(id: 'workspaces.privateWorkspace', source: 'Modules', package: - 'Neos.Neos')} + {neos:backend.translate(id: 'workspaces.privateWorkspace', source: 'Main', package: 'Neos.Workspace.Ui')} - {neos:backend.translate(id: 'workspaces.internalWorkspace', source: 'Modules', package: - 'Neos.Neos')} + {neos:backend.translate(id: 'workspaces.internalWorkspace', source: 'Main', package: 'Neos.Workspace.Ui')} --- @@ -96,8 +94,8 @@ - {nodeInformation.documentNode.label} + title="{neos:backend.translate(id: 'workspaces.openPageInWorkspace', source: 'Main', package: 'Neos.Workspace.Ui', arguments: {0: nodeInformation.workspace.workspaceTitle.value})}"> + {neos:node.label(node: nodeInformation.documentNode)} @@ -113,7 +111,7 @@ title="{f:if(condition: nodeInformation.node.nodeType.label, then: '{neos:backend.translate(id: nodeInformation.node.nodeType.label, package: \'Neos.Neos\')}', else: '{nodeInformation.node.nodeType.name}')}" data-neos-toggle="tooltip" data-placement="left"> - {nodeInformation.node.label} + {neos:node.label(node: nodeInformation.node)} @@ -124,19 +122,19 @@ diff --git a/Neos.Neos/Classes/AssetUsage/AssetUsageIndexingProcessor.php b/Neos.Neos/Classes/AssetUsage/AssetUsageIndexingProcessor.php new file mode 100644 index 00000000000..9a05c7260c8 --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/AssetUsageIndexingProcessor.php @@ -0,0 +1,88 @@ +getVariationGraph(); + + $workspaceFinder = $contentRepository->getWorkspaceFinder(); + $liveWorkspace = $workspaceFinder->findOneByName(WorkspaceName::forLive()); + if ($liveWorkspace === null) { + throw WorkspaceDoesNotExist::butWasSupposedTo(WorkspaceName::forLive()); + } + + $this->assetUsageIndexingService->pruneIndex($contentRepository->id); + + $workspaces = [$liveWorkspace]; + + $this->dispatchMessage($callback, sprintf('ContentRepository "%s"', $contentRepository->id->value)); + while ($workspaces !== []) { + $workspace = array_shift($workspaces); + + $contentGraph = $contentRepository->getContentGraph($workspace->workspaceName); + $this->dispatchMessage($callback, sprintf(' Workspace: %s', $contentGraph->getWorkspaceName()->value)); + + $dimensionSpacePoints = $variationGraph->getDimensionSpacePoints(); + + $rootNodeAggregate = $contentGraph->findRootNodeAggregateByType( + $nodeTypeName + ); + if ($rootNodeAggregate === null) { + $this->dispatchMessage($callback, sprintf(' ERROR: %s', "Root node aggregate was not found.")); + continue; + } + $rootNodeAggregateId = $rootNodeAggregate->nodeAggregateId; + + foreach ($dimensionSpacePoints as $dimensionSpacePoint) { + $this->dispatchMessage($callback, sprintf(' DimensionSpacePoint: %s', $dimensionSpacePoint->toJson())); + + $subgraph = $contentGraph->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + $childNodes = iterator_to_array($subgraph->findChildNodes($rootNodeAggregateId, FindChildNodesFilter::create())); + + while ($childNodes !== []) { + /** @var Node $childNode */ + $childNode = array_shift($childNodes); + if (!$childNode->originDimensionSpacePoint->equals($childNode->dimensionSpacePoint)) { + continue; + } + $this->assetUsageIndexingService->updateIndex($contentRepository->id, $childNode); + array_push($childNodes, ...iterator_to_array($subgraph->findChildNodes($childNode->aggregateId, FindChildNodesFilter::create()))); + } + } + + array_push($workspaces, ...array_values($workspaceFinder->findByBaseWorkspace($workspace->workspaceName))); + } + } + + private function dispatchMessage(?callable $callback, string $value): void + { + if ($callback === null) { + return; + } + + $callback($value); + } +} diff --git a/Neos.Neos/Classes/AssetUsage/AssetUsageService.php b/Neos.Neos/Classes/AssetUsage/AssetUsageService.php new file mode 100644 index 00000000000..a20dde844b4 --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/AssetUsageService.php @@ -0,0 +1,28 @@ +assetUsageRepository->findUsages($contentRepositoryId, $filter); + } +} diff --git a/Neos.Neos/Classes/AssetUsage/AssetUsageStrategy.php b/Neos.Neos/Classes/AssetUsage/AssetUsageStrategy.php index 00e4e2dde35..444d12a435f 100644 --- a/Neos.Neos/Classes/AssetUsage/AssetUsageStrategy.php +++ b/Neos.Neos/Classes/AssetUsage/AssetUsageStrategy.php @@ -51,7 +51,7 @@ public function getUsageReferences(AssetInterface $asset): array $convertedUsages[] = new AssetUsageReference( $asset, ContentRepositoryId::fromString($contentRepositoryId), - $usage->contentStreamId, + $usage->workspaceName, $usage->originDimensionSpacePoint, $usage->nodeAggregateId ); diff --git a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php new file mode 100644 index 00000000000..e240adb41bc --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php @@ -0,0 +1,162 @@ + We want to skip these events, because their workspace doesn't match current content stream. + try { + $contentGraph = $this->contentRepository->getContentGraph($eventInstance->getWorkspaceName()); + } catch (WorkspaceDoesNotExist) { + return; + } + if (!$contentGraph->getContentStreamId()->equals($eventInstance->getContentStreamId())) { + return; + } + } + + match ($eventInstance::class) { + NodeAggregateWasRemoved::class => $this->removeNodes($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->affectedCoveredDimensionSpacePoints), + WorkspaceWasPartiallyDiscarded::class => $this->discardNodes($eventInstance->getWorkspaceName(), $eventInstance->discardedNodes), + default => null + }; + } + + public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + { + if ($eventInstance instanceof EmbedsWorkspaceName && $eventInstance instanceof EmbedsContentStreamId) { + // Safeguard for temporary content streams created during partial publish -> We want to skip these events, because their workspace doesn't match current content stream. + try { + $contentGraph = $this->contentRepository->getContentGraph($eventInstance->getWorkspaceName()); + } catch (WorkspaceDoesNotExist) { + return; + } + if (!$contentGraph->getContentStreamId()->equals($eventInstance->getContentStreamId())) { + return; + } + } + + match ($eventInstance::class) { + NodeAggregateWithNodeWasCreated::class => $this->updateNode($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->originDimensionSpacePoint->toDimensionSpacePoint()), + NodePeerVariantWasCreated::class => $this->updateNode($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->peerOrigin->toDimensionSpacePoint()), + NodeGeneralizationVariantWasCreated::class => $this->updateNode($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->generalizationOrigin->toDimensionSpacePoint()), + NodeSpecializationVariantWasCreated::class => $this->updateNode($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->specializationOrigin->toDimensionSpacePoint()), + NodePropertiesWereSet::class => $this->updateNode($eventInstance->getWorkspaceName(), $eventInstance->nodeAggregateId, $eventInstance->originDimensionSpacePoint->toDimensionSpacePoint()), + WorkspaceWasDiscarded::class => $this->discardWorkspace($eventInstance->getWorkspaceName()), + DimensionSpacePointWasMoved::class => $this->updateDimensionSpacePoint($eventInstance->getWorkspaceName(), $eventInstance->source, $eventInstance->target), + default => null + }; + } + + + public function onBeforeBatchCompleted(): void + { + } + + public function onAfterCatchUp(): void + { + } + + private function updateNode(WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, DimensionSpacePoint $dimensionSpacePoint): void + { + $contentGraph = $this->contentRepository->getContentGraph($workspaceName); + $node = $contentGraph->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->findNodeById($nodeAggregateId); + + if ($node === null) { + // Node not found, nothing to do here. + return; + } + + $this->assetUsageIndexingService->updateIndex( + $this->contentRepository->id, + $node + ); + } + + private function removeNodes(WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $dimensionSpacePoints): void + { + $contentGraph = $this->contentRepository->getContentGraph($workspaceName); + + foreach ($dimensionSpacePoints as $dimensionSpacePoint) { + $subgraph = $contentGraph->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + $node = $subgraph->findNodeById($nodeAggregateId); + $descendants = $subgraph->findDescendantNodes($nodeAggregateId, FindDescendantNodesFilter::create()); + + $nodes = array_merge([$node], iterator_to_array($descendants)); + + /** @var Node $node */ + foreach ($nodes as $node) { + $this->assetUsageIndexingService->removeIndexForNode( + $this->contentRepository->id, + $node + ); + } + } + } + + private function discardWorkspace(WorkspaceName $workspaceName): void + { + $this->assetUsageIndexingService->removeIndexForWorkspace($this->contentRepository->id, $workspaceName); + } + + private function discardNodes(WorkspaceName $workspaceName, NodeIdsToPublishOrDiscard $nodeIds): void + { + foreach ($nodeIds as $nodeId) { + $this->assetUsageIndexingService->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( + $this->contentRepository->id, + $workspaceName, + $nodeId->nodeAggregateId, + $nodeId->dimensionSpacePoint + ); + } + } + + private function updateDimensionSpacePoint(WorkspaceName $workspaceName, DimensionSpacePoint $source, DimensionSpacePoint $target): void + { + $this->assetUsageIndexingService->updateDimensionSpacePointInIndex($this->contentRepository->id, $workspaceName, $source, $target); + } +} diff --git a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHookFactory.php b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHookFactory.php new file mode 100644 index 00000000000..89bcec32e86 --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHookFactory.php @@ -0,0 +1,35 @@ +assetUsageIndexingService + ); + } +} diff --git a/Neos.Neos/Classes/AssetUsage/Command/AssetUsageCommandController.php b/Neos.Neos/Classes/AssetUsage/Command/AssetUsageCommandController.php index 2997cabdb70..043edc809b9 100644 --- a/Neos.Neos/Classes/AssetUsage/Command/AssetUsageCommandController.php +++ b/Neos.Neos/Classes/AssetUsage/Command/AssetUsageCommandController.php @@ -4,64 +4,37 @@ namespace Neos\Neos\AssetUsage\Command; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Cli\CommandController; -use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageRepositoryFactory; -use Neos\Neos\AssetUsage\Service\AssetUsageSyncServiceFactory; +use Neos\Neos\AssetUsage\AssetUsageIndexingProcessor; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; final class AssetUsageCommandController extends CommandController { public function __construct( - private readonly AssetRepository $assetRepository, - private readonly AssetUsageRepositoryFactory $assetUsageRepositoryFactory, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly AssetUsageIndexingProcessor $assetUsageIndexingProcessor ) { parent::__construct(); } - /** - * Remove asset usages that are no longer valid - * - * This is the case for usages that refer to - * * deleted nodes (i.e. nodes that were implicitly removed because an ancestor node was deleted) - * * invalid dimension space points (e.g. because dimension configuration has been changed) - * * removed content streams - * - * @param bool $quiet if Set, only errors will be outputted - */ - public function syncCommand(string $contentRepository = 'default', bool $quiet = false): void + public function indexCommand(string $contentRepository = 'default', string $nodeTypeName = NodeTypeNameFactory::NAME_SITES): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $assetUsageSyncService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new AssetUsageSyncServiceFactory( - $this->assetRepository, - $this->assetUsageRepositoryFactory - ) - ); + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $usages = $assetUsageSyncService->findAllUsages(); - if (!$quiet) { - $this->output->progressStart($usages->count()); - } - $numberOfRemovedUsages = 0; - foreach ($usages as $usage) { - if (!$assetUsageSyncService->isAssetUsageStillValid($usage)) { - $assetUsageSyncService->removeAssetUsage($usage); - $numberOfRemovedUsages++; - } - if (!$quiet) { - $this->output->progressAdvance(); + $this->outputFormatted("Start indexing asset usages"); + + $this->assetUsageIndexingProcessor->buildIndex( + $contentRepository, + NodeTypeName::fromString($nodeTypeName), + function (string $message) { + $this->outputFormatted($message); } - } - if (!$quiet) { - $this->output->progressFinish(); - $this->outputLine(); - $this->outputLine('Removed %d asset usage%s', [ - $numberOfRemovedUsages, $numberOfRemovedUsages === 1 ? '' : 's' - ]); - } + ); + + $this->outputFormatted("Finished."); } } diff --git a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsage.php b/Neos.Neos/Classes/AssetUsage/Domain/AssetUsage.php similarity index 63% rename from Neos.Neos/Classes/AssetUsage/Dto/AssetUsage.php rename to Neos.Neos/Classes/AssetUsage/Domain/AssetUsage.php index 82aaaece5c9..3f0eea0cd51 100644 --- a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsage.php +++ b/Neos.Neos/Classes/AssetUsage/Domain/AssetUsage.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace Neos\Neos\AssetUsage\Dto; +namespace Neos\Neos\AssetUsage\Domain; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; /** @@ -16,8 +17,9 @@ final readonly class AssetUsage { public function __construct( + public ContentRepositoryId $contentRepositoryId, public string $assetId, - public ContentStreamId $contentStreamId, + public WorkspaceName $workspaceName, public OriginDimensionSpacePoint $originDimensionSpacePoint, public NodeAggregateId $nodeAggregateId, public string $propertyName, diff --git a/Neos.Neos/Classes/AssetUsage/Domain/AssetUsageRepository.php b/Neos.Neos/Classes/AssetUsage/Domain/AssetUsageRepository.php new file mode 100644 index 00000000000..23e13ffcec6 --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/Domain/AssetUsageRepository.php @@ -0,0 +1,259 @@ +dbal->createQueryBuilder(); + $queryBuilder + ->select('*') + ->from(self::TABLE); + $queryBuilder->andWhere('contentrepositoryid = :contentRepositoryId'); + $queryBuilder->setParameter('contentRepositoryId', $contentRepositoryId->value); + if ($filter->hasAssetId()) { + if ($filter->includeVariantsOfAsset === true) { + $queryBuilder->andWhere( + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq('assetid', ':assetId'), + $queryBuilder->expr()->eq('originalassetid', ':assetId'), + ) + ); + } else { + $queryBuilder->andWhere('assetid = :assetId'); + } + + $queryBuilder->setParameter('assetId', $filter->assetId); + } + if ($filter->hasWorkspaceName()) { + $queryBuilder->andWhere('workspacename = :workspaceName'); + $queryBuilder->setParameter('workspaceName', $filter->workspaceName?->value); + } + if ($filter->groupByAsset) { + $queryBuilder->addGroupBy('assetid'); + } + if ($filter->groupByNodeAggregate) { + $queryBuilder->addGroupBy('nodeaggregateid'); + } + if ($filter->groupByWorkspaceName) { + $queryBuilder->addGroupBy('workspacename'); + } + if ($filter->groupByNode) { + $queryBuilder->addGroupBy('nodeaggregateid'); + $queryBuilder->addGroupBy('origindimensionspacepointhash'); + } + return new AssetUsages(function () use ($queryBuilder) { + $result = $queryBuilder->execute(); + if (!$result instanceof Result) { + throw new \RuntimeException(sprintf( + 'Expected instance of "%s", got: "%s"', + Result::class, + get_debug_type($result) + ), 1646320966); + } + /** @var array{contentrepositoryid: string,assetid: string, workspacename: string, origindimensionspacepointhash: string, origindimensionspacepoint: string, nodeaggregateid: string, propertyname: string} $row */ + foreach ($result->iterateAssociative() as $row) { + yield new AssetUsage( + ContentRepositoryId::fromString($row['contentrepositoryid']), + $row['assetid'], + WorkspaceName::fromString($row['workspacename']), + OriginDimensionSpacePoint::fromJsonString($row['origindimensionspacepoint']), + NodeAggregateId::fromString($row['nodeaggregateid']), + $row['propertyname'] + ); + } + }, function () use ($queryBuilder) { + /** @var string $count */ + $count = $this->dbal->fetchOne( + 'SELECT COUNT(*) FROM (' . $queryBuilder->getSQL() . ') s', + $queryBuilder->getParameters() + ); + return (int)$count; + }); + } + + /** + * @param WorkspaceName[] $workspaceNames + * @return array + */ + public function findUsageForNodeInWorkspaces(ContentRepositoryId $contentRepositoryId, Node $node, array $workspaceNames): array + { + $sql = <<getTableName()} + WHERE + contentrepositoryid = :contentRepositoryId + AND nodeaggregateid = :nodeAggregateId + AND origindimensionspacepointhash = :originDimensionSpacePointHash + AND workspacename in (:workspaceNames) + SQL; + + $result = $this->dbal->executeQuery($sql, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'nodeAggregateId' => $node->aggregateId->value, + 'originDimensionSpacePointHash' => $node->dimensionSpacePoint->hash, + 'workspaceNames' => array_map(fn ($workspaceName) => $workspaceName->value, $workspaceNames), + ], [ + 'propertyNames' => Connection::PARAM_STR_ARRAY, + 'workspaceNames' => Connection::PARAM_STR_ARRAY, + ]); + + $usages = []; + foreach ($result->iterateAssociative() as $row) { + $usages[] = new AssetUsage( + ContentRepositoryId::fromString($row['contentrepositoryid']), + $row['assetid'], + WorkspaceName::fromString($row['workspacename']), + OriginDimensionSpacePoint::fromJsonString($row['origindimensionspacepoint']), + NodeAggregateId::fromString($row['nodeaggregateid']), + $row['propertyname'] + ); + } + return $usages; + } + + public function addUsagesForNodeWithAssetOnProperty(ContentRepositoryId $contentRepositoryId, Node $node, string $propertyName, string $assetId, ?string $originalAssetId = null): void + { + try { + $this->dbal->insert(self::TABLE, [ + 'contentrepositoryid' => $contentRepositoryId->value, + 'assetid' => $assetId, + 'originalassetid' => $originalAssetId, + 'workspacename' => $node->workspaceName->value, + 'nodeaggregateid' => $node->aggregateId->value, + 'origindimensionspacepoint' => $node->dimensionSpacePoint->toJson(), + 'origindimensionspacepointhash' => $node->dimensionSpacePoint->hash, + 'propertyname' => $propertyName, + ]); + } catch (UniqueConstraintViolationException $e) { + // A usage already exists for this node and property -> can be ignored + } + } + + public function updateAssetUsageDimensionSpacePoint(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, DimensionSpacePoint $source, DimensionSpacePoint $target): void + { + $this->dbal->update($this->getTableName(), [ + 'origindimensionspacepoint' => $target->toJson(), + 'origindimensionspacepointhash' => $target->hash, + ], [ + 'contentrepositoryid' => $contentRepositoryId->value, + 'workspacename' => $workspaceName->value, + 'origindimensionspacepointhash' => $source->hash, + ]); + } + + public function removeAssetUsagesOfWorkspace( + ContentRepositoryId $contentRepositoryId, + WorkspaceName $workspaceName, + ): void { + $sql = <<getTableName()} + WHERE contentrepositoryid = :contentRepositoryId + AND workspacename = :workspaceName + SQL; + + $this->dbal->executeStatement($sql, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + ]); + } + + public function removeAssetUsagesOfWorkspaceWithAllProperties( + ContentRepositoryId $contentRepositoryId, + WorkspaceName $workspaceName, + NodeAggregateId $nodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint + ): void { + $sql = <<getTableName()} + WHERE contentrepositoryid = :contentRepositoryId + AND workspacename = :workspaceName + AND nodeAggregateId = :nodeAggregateId + AND originDimensionSpacePointHash = :originDimensionSpacePointHash + SQL; + + $this->dbal->executeStatement($sql, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + 'nodeAggregateId' => $nodeAggregateId->value, + 'originDimensionSpacePointHash' => $dimensionSpacePoint->hash, + ]); + } + + /** + * @param WorkspaceName[] $workspaceNames + */ + public function removeAssetUsagesForNodeAggregateIdAndDimensionSpacePointWithAssetOnPropertyInWorkspaces( + ContentRepositoryId $contentRepositoryId, + NodeAggregateId $nodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint, + string $propertyName, + string $assetId, + array $workspaceNames + ): void { + $sql = <<getTableName()} + WHERE contentrepositoryid = :contentRepositoryId + AND workspacename in (:workspaceNames) + AND nodeaggregateid = :nodeAggregateId + AND origindimensionspacepointhash = :originDimensionSpacePointHash + AND propertyname = :propertyName + AND assetId = :assetId + SQL; + + $this->dbal->executeStatement($sql, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'nodeAggregateId' => $nodeAggregateId->value, + 'originDimensionSpacePointHash' => $dimensionSpacePoint->hash, + 'workspaceNames' => array_map(fn ($workspaceName) => $workspaceName->value, $workspaceNames), + 'propertyName' => $propertyName, + 'assetId' => $assetId, + ], [ + 'workspaceNames' => Connection::PARAM_STR_ARRAY, + ]); + } + + public function removeAsset(ContentRepositoryId $contentRepositoryId, string $assetId): void + { + $this->dbal->delete(self::TABLE, [ + 'contentrepositoryid' => $contentRepositoryId->value, + 'assetId' => $assetId, + ]); + } + + public function removeAll(ContentRepositoryId $contentRepositoryId): void + { + $this->dbal->delete(self::TABLE, [ + 'contentrepositoryid' => $contentRepositoryId->value, + ]); + } + + private function getTableName(): string + { + return self::TABLE; + } +} diff --git a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageFilter.php b/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageFilter.php index 1e65ab39f75..92822bf969c 100644 --- a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageFilter.php +++ b/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageFilter.php @@ -4,7 +4,7 @@ namespace Neos\Neos\AssetUsage\Dto; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; /** @@ -15,41 +15,53 @@ { private function __construct( public ?string $assetId, - public ?ContentStreamId $contentStreamId, + public ?WorkspaceName $workspaceName, public bool $groupByAsset, public bool $groupByNode, + public bool $groupByNodeAggregate, + public bool $groupByWorkspaceName, public bool $includeVariantsOfAsset, ) { } public static function create(): self { - return new self(null, null, false, false, false); + return new self(null, null, false, false, false, false, false); } public function withAsset(string $assetId): self { - return new self($assetId, $this->contentStreamId, $this->groupByAsset, $this->groupByNode, $this->includeVariantsOfAsset); + return new self($assetId, $this->workspaceName, $this->groupByAsset, $this->groupByNode, $this->groupByNodeAggregate, $this->groupByWorkspaceName, $this->includeVariantsOfAsset); } - public function withContentStream(ContentStreamId $contentStreamId): self + public function withWorkspaceName(WorkspaceName $workspaceName): self { - return new self($this->assetId, $contentStreamId, $this->groupByAsset, $this->groupByNode, $this->includeVariantsOfAsset); + return new self($this->assetId, $workspaceName, $this->groupByAsset, $this->groupByNode, $this->groupByNodeAggregate, $this->groupByWorkspaceName, $this->includeVariantsOfAsset); } public function includeVariantsOfAsset(): self { - return new self($this->assetId, $this->contentStreamId, $this->groupByAsset, $this->groupByNode, true); + return new self($this->assetId, $this->workspaceName, $this->groupByAsset, $this->groupByNode, $this->groupByNodeAggregate, $this->groupByWorkspaceName, true); } public function groupByAsset(): self { - return new self($this->assetId, $this->contentStreamId, true, $this->groupByNode, $this->includeVariantsOfAsset); + return new self($this->assetId, $this->workspaceName, true, $this->groupByNode, $this->groupByNodeAggregate, $this->groupByWorkspaceName, $this->includeVariantsOfAsset); } public function groupByNode(): self { - return new self($this->assetId, $this->contentStreamId, $this->groupByAsset, true, $this->includeVariantsOfAsset); + return new self($this->assetId, $this->workspaceName, $this->groupByAsset, true, $this->groupByNodeAggregate, $this->groupByWorkspaceName, $this->includeVariantsOfAsset); + } + + public function groupByNodeAggregate(): self + { + return new self($this->assetId, $this->workspaceName, $this->groupByAsset, $this->groupByNode, true, $this->groupByWorkspaceName, $this->includeVariantsOfAsset); + } + + public function groupByWorkspaceName(): self + { + return new self($this->assetId, $this->workspaceName, $this->groupByAsset, $this->groupByNode, $this->groupByNodeAggregate, true, $this->includeVariantsOfAsset); } public function hasAssetId(): bool @@ -57,8 +69,8 @@ public function hasAssetId(): bool return $this->assetId !== null; } - public function hasContentStreamId(): bool + public function hasWorkspaceName(): bool { - return $this->contentStreamId !== null; + return $this->workspaceName !== null; } } diff --git a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageNodeAddress.php b/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageNodeAddress.php deleted file mode 100644 index a501ce52b38..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsageNodeAddress.php +++ /dev/null @@ -1,29 +0,0 @@ -contentRepositoryId; } - public function getContentStreamId(): ContentStreamId + public function getWorkspaceName(): WorkspaceName { - return $this->contentStreamId; + return $this->workspaceName; } public function getOriginDimensionSpacePoint(): OriginDimensionSpacePoint diff --git a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsages.php b/Neos.Neos/Classes/AssetUsage/Dto/AssetUsages.php index fe2ca514853..a3c6870d5bd 100644 --- a/Neos.Neos/Classes/AssetUsage/Dto/AssetUsages.php +++ b/Neos.Neos/Classes/AssetUsage/Dto/AssetUsages.php @@ -5,6 +5,7 @@ namespace Neos\Neos\AssetUsage\Dto; use Neos\Flow\Annotations as Flow; +use Neos\Neos\AssetUsage\Domain\AssetUsage; /** * @implements \IteratorAggregate diff --git a/Neos.Neos/Classes/AssetUsage/GlobalAssetUsageService.php b/Neos.Neos/Classes/AssetUsage/GlobalAssetUsageService.php index db5a13af4e3..f7121535b13 100644 --- a/Neos.Neos/Classes/AssetUsage/GlobalAssetUsageService.php +++ b/Neos.Neos/Classes/AssetUsage/GlobalAssetUsageService.php @@ -5,15 +5,12 @@ namespace Neos\Neos\AssetUsage; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Neos\AssetUsage\Domain\AssetUsageRepository; use Neos\Neos\AssetUsage\Dto\AssetUsageFilter; use Neos\Neos\AssetUsage\Dto\AssetUsagesByContentRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; -use Neos\Neos\AssetUsage\Projection\AssetUsageRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageRepositoryFactory; /** * Central authority to look up or remove asset usages in all configured Content Repositories @@ -21,24 +18,19 @@ * @api This is used by the {@see AssetUsageStrategy} */ #[Flow\Scope('singleton')] -class GlobalAssetUsageService implements ContentRepositoryServiceInterface +class GlobalAssetUsageService { /** * @var array */ private ?array $contentRepositories = null; - /** - * @var array - */ - private array $assetUsageRepositories = []; - /** * @param array $contentRepositoryIds in the format ['' => true, '' => false] */ public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly AssetUsageRepositoryFactory $assetUsageRepositoryFactory, + private readonly AssetUsageRepository $assetUsageRepository, private readonly array $contentRepositoryIds ) { } @@ -47,7 +39,7 @@ public function findByFilter(AssetUsageFilter $filter): AssetUsagesByContentRepo { $assetUsages = []; foreach ($this->getContentRepositories() as $contentRepositoryId => $contentRepository) { - $assetUsages[$contentRepositoryId] = $contentRepository->projectionState(AssetUsageFinder::class)->findByFilter($filter); + $assetUsages[$contentRepositoryId] = $this->assetUsageRepository->findUsages($contentRepository->id, $filter); } return new AssetUsagesByContentRepository($assetUsages); } @@ -55,7 +47,7 @@ public function findByFilter(AssetUsageFilter $filter): AssetUsagesByContentRepo public function removeAssetUsageByAssetId(string $assetId): void { foreach ($this->getContentRepositories() as $contentRepositoryId => $contentRepository) { - $this->getAssetUsageRepository(ContentRepositoryId::fromString($contentRepositoryId))->removeAsset($assetId); + $this->assetUsageRepository->removeAsset(ContentRepositoryId::fromString($contentRepositoryId), $assetId); } } @@ -81,13 +73,4 @@ private function getContentRepositories(): array return $this->contentRepositories; } - - private function getAssetUsageRepository(ContentRepositoryId $contentRepositoryId): AssetUsageRepository - { - if (!array_key_exists($contentRepositoryId->value, $this->assetUsageRepositories)) { - $this->assetUsageRepositories[$contentRepositoryId->value] = $this->assetUsageRepositoryFactory->build($contentRepositoryId); - } - - return $this->assetUsageRepositories[$contentRepositoryId->value]; - } } diff --git a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageFinder.php b/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageFinder.php deleted file mode 100644 index 3968832dafc..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageFinder.php +++ /dev/null @@ -1,35 +0,0 @@ -getProjectionState(AssetUsageProjection::class) - * - * To look up usages for all configured Content Repositories, use {@see GlobalAssetUsageService} instead - * - * @api - */ -final class AssetUsageFinder implements ProjectionStateInterface -{ - public function __construct( - private readonly AssetUsageRepository $repository, - ) { - } - - public function findByFilter(AssetUsageFilter $filter): AssetUsages - { - return $this->repository->findUsages($filter); - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjection.php b/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjection.php deleted file mode 100644 index ec762433ff9..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjection.php +++ /dev/null @@ -1,319 +0,0 @@ - - * @internal - */ -final class AssetUsageProjection implements ProjectionInterface -{ - private ?AssetUsageFinder $stateAccessor = null; - private AssetUsageRepository $repository; - private DbalCheckpointStorage $checkpointStorage; - /** @var array */ - private array $originalAssetIdMappingRuntimeCache = []; - - public function __construct( - private readonly AssetRepository $assetRepository, - ContentRepositoryId $contentRepositoryId, - Connection $dbal, - AssetUsageRepositoryFactory $assetUsageRepositoryFactory, - ) { - $this->repository = $assetUsageRepositoryFactory->build($contentRepositoryId); - $this->checkpointStorage = new DbalCheckpointStorage( - $dbal, - $this->repository->getTableNamePrefix() . '_checkpoint', - self::class - ); - } - - public function reset(): void - { - $this->repository->reset(); - $this->checkpointStorage->acquireLock(); - $this->checkpointStorage->updateAndReleaseLock(SequenceNumber::none()); - } - - public function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void - { - try { - $assetIdsByProperty = $this->getAssetIdsByProperty($event->initialPropertyValues); - } catch (InvalidTypeException $e) { - throw new \RuntimeException( - sprintf( - 'Failed to extract asset ids from event "%s": %s', - $eventEnvelope->event->id->value, - $e->getMessage() - ), - 1646321894, - $e - ); - } - $nodeAddress = new AssetUsageNodeAddress( - $event->getContentStreamId(), - $event->getOriginDimensionSpacePoint()->toDimensionSpacePoint(), - $event->getNodeAggregateId() - ); - $this->repository->addUsagesForNode($nodeAddress, $assetIdsByProperty); - } - - public function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void - { - try { - $assetIdsByProperty = $this->getAssetIdsByProperty($event->propertyValues); - } catch (InvalidTypeException $e) { - throw new \RuntimeException( - sprintf( - 'Failed to extract asset ids from event "%s": %s', - $eventEnvelope->event->id->value, - $e->getMessage() - ), - 1646321894, - $e - ); - } - $nodeAddress = new AssetUsageNodeAddress( - $event->getContentStreamId(), - $event->getOriginDimensionSpacePoint()->toDimensionSpacePoint(), - $event->getNodeAggregateId() - ); - $this->repository->addUsagesForNode($nodeAddress, $assetIdsByProperty); - } - - public function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void - { - $this->repository->removeNode( - $event->getNodeAggregateId(), - $event->affectedOccupiedDimensionSpacePoints->toDimensionSpacePointSet() - ); - } - - - public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event): void - { - $this->repository->copyDimensions($event->sourceOrigin, $event->peerOrigin); - } - - public function whenContentStreamWasForked(ContentStreamWasForked $event): void - { - $this->repository->copyContentStream( - $event->sourceContentStreamId, - $event->newContentStreamId - ); - } - - public function whenWorkspaceWasDiscarded(WorkspaceWasDiscarded $event): void - { - $this->repository->removeContentStream($event->previousContentStreamId); - } - - public function whenWorkspaceWasPartiallyDiscarded(WorkspaceWasPartiallyDiscarded $event): void - { - $this->repository->removeContentStream($event->previousContentStreamId); - } - - public function whenWorkspaceWasPartiallyPublished(WorkspaceWasPartiallyPublished $event): void - { - $this->repository->removeContentStream($event->previousSourceContentStreamId); - } - - public function whenWorkspaceWasPublished(WorkspaceWasPublished $event): void - { - $this->repository->removeContentStream($event->previousSourceContentStreamId); - } - - public function whenWorkspaceWasRebased(WorkspaceWasRebased $event): void - { - $this->repository->removeContentStream($event->previousContentStreamId); - } - - public function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void - { - $this->repository->removeContentStream($event->contentStreamId); - } - - - // ---------------- - - /** - * @throws InvalidTypeException - */ - private function getAssetIdsByProperty(SerializedPropertyValues $propertyValues): AssetIdsByProperty - { - /** @var array> $assetIds */ - $assetIds = []; - foreach ($propertyValues as $propertyName => $propertyValue) { - $extractedAssetIds = $this->extractAssetIds( - $propertyValue->type, - $propertyValue->value, - ); - - $assetIds[$propertyName] = array_map( - fn($assetId) => new AssetIdAndOriginalAssetId($assetId, $this->findOriginalAssetId($assetId)), - $extractedAssetIds - ); - } - return new AssetIdsByProperty($assetIds); - } - - /** - * @param mixed $value - * @return array - * @throws InvalidTypeException - */ - private function extractAssetIds(string $type, mixed $value): array - { - if (is_string($value)) { - preg_match_all('/asset:\/\/(?[\w-]*)/i', $value, $matches, PREG_SET_ORDER); - return array_map(static fn(array $match) => $match['assetId'], $matches); - } - if (is_subclass_of($type, ResourceBasedInterface::class)) { - return isset($value['__identifier']) ? [$value['__identifier']] : []; - } - - // Collection type? - /** @var array{type: string, elementType: string|null, nullable: bool} $parsedType */ - $parsedType = TypeHandling::parseType($type); - if ($parsedType['elementType'] === null) { - return []; - } - if ( - !is_subclass_of($parsedType['elementType'], ResourceBasedInterface::class) - && !is_subclass_of($parsedType['elementType'], \Stringable::class) - ) { - return []; - } - /** @var array> $assetIds */ - $assetIds = []; - /** @var iterable $value */ - foreach ($value as $elementValue) { - $assetIds[] = $this->extractAssetIds($parsedType['elementType'], $elementValue); - } - return array_merge(...$assetIds); - } - - public function setUp(): void - { - $this->repository->setUp(); - $this->checkpointStorage->setUp(); - } - - public function status(): ProjectionStatus - { - $checkpointStorageStatus = $this->checkpointStorage->status(); - if ($checkpointStorageStatus->type === CheckpointStorageStatusType::ERROR) { - return ProjectionStatus::error($checkpointStorageStatus->details); - } - if ($checkpointStorageStatus->type === CheckpointStorageStatusType::SETUP_REQUIRED) { - return ProjectionStatus::setupRequired($checkpointStorageStatus->details); - } - try { - $falseOrDetailsString = $this->repository->isSetupRequired(); - if (is_string($falseOrDetailsString)) { - return ProjectionStatus::setupRequired($falseOrDetailsString); - } - } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); - } - return ProjectionStatus::ok(); - } - - public function canHandle(EventInterface $event): bool - { - return in_array($event::class, [ - NodeAggregateWithNodeWasCreated::class, - NodePropertiesWereSet::class, - NodeAggregateWasRemoved::class, - NodePeerVariantWasCreated::class, - ContentStreamWasForked::class, - WorkspaceWasDiscarded::class, - WorkspaceWasPartiallyDiscarded::class, - WorkspaceWasPartiallyPublished::class, - WorkspaceWasPublished::class, - WorkspaceWasRebased::class, - ContentStreamWasRemoved::class, - ]); - } - - public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void - { - match ($event::class) { - NodeAggregateWithNodeWasCreated::class => $this->whenNodeAggregateWithNodeWasCreated($event, $eventEnvelope), - NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event, $eventEnvelope), - NodeAggregateWasRemoved::class => $this->whenNodeAggregateWasRemoved($event), - NodePeerVariantWasCreated::class => $this->whenNodePeerVariantWasCreated($event), - ContentStreamWasForked::class => $this->whenContentStreamWasForked($event), - WorkspaceWasDiscarded::class => $this->whenWorkspaceWasDiscarded($event), - WorkspaceWasPartiallyDiscarded::class => $this->whenWorkspaceWasPartiallyDiscarded($event), - WorkspaceWasPartiallyPublished::class => $this->whenWorkspaceWasPartiallyPublished($event), - WorkspaceWasPublished::class => $this->whenWorkspaceWasPublished($event), - WorkspaceWasRebased::class => $this->whenWorkspaceWasRebased($event), - ContentStreamWasRemoved::class => $this->whenContentStreamWasRemoved($event), - default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))), - }; - } - - public function getCheckpointStorage(): DbalCheckpointStorage - { - return $this->checkpointStorage; - } - - public function getState(): AssetUsageFinder - { - if (!$this->stateAccessor) { - $this->stateAccessor = new AssetUsageFinder($this->repository); - } - return $this->stateAccessor; - } - - private function findOriginalAssetId(string $assetId): ?string - { - if (!array_key_exists($assetId, $this->originalAssetIdMappingRuntimeCache)) { - try { - /** @var AssetInterface|null $asset */ - $asset = $this->assetRepository->findByIdentifier($assetId); - } /** @noinspection PhpRedundantCatchClauseInspection */ catch (ORMException) { - return null; - } - /** @phpstan-ignore-next-line */ - $this->originalAssetIdMappingRuntimeCache[$assetId] = $asset instanceof AssetVariantInterface ? $asset->getOriginalAsset()->getIdentifier() : null; - } - - return $this->originalAssetIdMappingRuntimeCache[$assetId]; - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjectionFactory.php b/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjectionFactory.php deleted file mode 100644 index d49df6574fc..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageProjectionFactory.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @internal - */ -final class AssetUsageProjectionFactory implements ProjectionFactoryInterface -{ - public function __construct( - private readonly Connection $dbal, - private readonly AssetUsageRepositoryFactory $assetUsageRepositoryFactory, - private readonly AssetRepository $assetRepository, - ) { - } - - public function build( - ProjectionFactoryDependencies $projectionFactoryDependencies, - array $options, - ): AssetUsageProjection { - return new AssetUsageProjection( - $this->assetRepository, - $projectionFactoryDependencies->contentRepositoryId, - $this->dbal, - $this->assetUsageRepositoryFactory, - ); - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepository.php b/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepository.php deleted file mode 100644 index e650fe81daf..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepository.php +++ /dev/null @@ -1,285 +0,0 @@ -dbal, $this->databaseSchema()) as $statement) { - $this->dbal->executeStatement($statement); - } - } - - /** - * @return false|non-empty-string false if everything is okay, otherwise the details string, why a setup is required - */ - public function isSetupRequired(): false|string - { - $requiredSqlStatements = DbalSchemaDiff::determineRequiredSqlStatements($this->dbal, $this->databaseSchema()); - if ($requiredSqlStatements !== []) { - return sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements)); - } - return false; - } - - private function databaseSchema(): Schema - { - $schemaManager = $this->dbal->createSchemaManager(); - $table = new Table($this->tableNamePrefix, [ - (new Column('assetid', Type::getType(Types::STRING)))->setLength(40)->setNotnull(true)->setDefault(''), - (new Column('originalassetid', Type::getType(Types::STRING)))->setLength(40)->setNotnull(false)->setDefault(null), - DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotNull(true), - DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotNull(true), - DbalSchemaFactory::columnForDimensionSpacePoint('origindimensionspacepoint')->setNotNull(false), - DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotNull(true), - (new Column('propertyname', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setDefault('') - ]); - - $table - ->addUniqueIndex(['assetid', 'originalassetid', 'contentstreamid', 'nodeaggregateid', 'origindimensionspacepointhash', 'propertyname'], 'assetperproperty') - ->addIndex(['assetid']) - ->addIndex(['originalassetid']) - ->addIndex(['contentstreamid']) - ->addIndex(['nodeaggregateid']) - ->addIndex(['origindimensionspacepointhash']); - - return DbalSchemaFactory::createSchemaWithTables($schemaManager, [$table]); - } - - public function findUsages(AssetUsageFilter $filter): AssetUsages - { - $queryBuilder = $this->dbal->createQueryBuilder(); - $queryBuilder - ->select('*') - ->from($this->tableNamePrefix); - if ($filter->hasAssetId()) { - if ($filter->includeVariantsOfAsset === true) { - $queryBuilder->andWhere( - $queryBuilder->expr()->or( - $queryBuilder->expr()->eq('assetId', ':assetId'), - $queryBuilder->expr()->eq('originalAssetId', ':assetId'), - ) - ); - } else { - $queryBuilder->andWhere('assetId = :assetId'); - } - - $queryBuilder->setParameter('assetId', $filter->assetId); - } - if ($filter->hasContentStreamId()) { - $queryBuilder->andWhere('contentStreamId = :contentStreamId'); - $queryBuilder->setParameter('contentStreamId', $filter->contentStreamId?->value); - } - if ($filter->groupByAsset) { - $queryBuilder->addGroupBy('assetId'); - } - if ($filter->groupByNode) { - $queryBuilder->addGroupBy('nodeaggregateid'); - $queryBuilder->addGroupBy('origindimensionspacepointhash'); - } - return new AssetUsages(function () use ($queryBuilder) { - $result = $queryBuilder->execute(); - if (!$result instanceof Result) { - throw new \RuntimeException(sprintf( - 'Expected instance of "%s", got: "%s"', - Result::class, - get_debug_type($result) - ), 1646320966); - } - /** @var array{assetid: string, contentstreamid: string, origindimensionspacepointhash: string, origindimensionspacepoint: string, nodeaggregateid: string, propertyname: string} $row */ - foreach ($result->iterateAssociative() as $row) { - yield new AssetUsage( - $row['assetid'], - ContentStreamId::fromString($row['contentstreamid']), - OriginDimensionSpacePoint::fromJsonString($row['origindimensionspacepoint']), - NodeAggregateId::fromString($row['nodeaggregateid']), - $row['propertyname'] - ); - } - }, function () use ($queryBuilder) { - /** @var string $count */ - $count = $this->dbal->fetchOne( - 'SELECT COUNT(*) FROM (' . $queryBuilder->getSQL() . ') s', - $queryBuilder->getParameters() - ); - return (int)$count; - }); - } - - public function addUsagesForNode(AssetUsageNodeAddress $nodeAddress, AssetIdsByProperty $assetIdsByProperty): void - { - // Delete all asset usage entries for newly set properties to ensure that removed or replaced assets are reflected - $this->dbal->executeStatement('DELETE FROM ' . $this->tableNamePrefix - . ' WHERE contentStreamId = :contentStreamId' - . ' AND nodeAggregateId = :nodeAggregateId' - . ' AND originDimensionSpacePointHash = :originDimensionSpacePointHash' - . ' AND propertyName IN (:propertyNames)', [ - 'contentStreamId' => $nodeAddress->contentStreamId->value, - 'nodeAggregateId' => $nodeAddress->nodeAggregateId->value, - 'originDimensionSpacePointHash' => $nodeAddress->dimensionSpacePoint->hash, - 'propertyNames' => $assetIdsByProperty->propertyNames(), - ], [ - 'propertyNames' => Connection::PARAM_STR_ARRAY, - ]); - - foreach ($assetIdsByProperty as $propertyName => $assetIdAndOriginalAssetIds) { - /** @var AssetIdAndOriginalAssetId $assetIdAndOriginalAssetId */ - foreach ($assetIdAndOriginalAssetIds as $assetIdAndOriginalAssetId) { - try { - $this->dbal->insert($this->tableNamePrefix, [ - 'assetId' => $assetIdAndOriginalAssetId->assetId, - 'originalAssetId' => $assetIdAndOriginalAssetId->originalAssetId, - 'contentStreamId' => $nodeAddress->contentStreamId->value, - 'nodeAggregateId' => $nodeAddress->nodeAggregateId->value, - 'originDimensionSpacePoint' => $nodeAddress->dimensionSpacePoint->toJson(), - 'originDimensionSpacePointHash' => $nodeAddress->dimensionSpacePoint->hash, - 'propertyName' => $propertyName, - ]); - } catch (UniqueConstraintViolationException $e) { - // A usage already exists for this node and property -> can be ignored - } - } - } - } - - public function removeContentStream(ContentStreamId $contentStreamId): void - { - $this->dbal->delete($this->tableNamePrefix, ['contentStreamId' => $contentStreamId->value]); - } - - public function copyContentStream( - ContentStreamId $sourceContentStreamId, - ContentStreamId $targetContentStreamId, - ): void { - $this->dbal->executeStatement( - 'INSERT INTO ' . $this->tableNamePrefix . ' (assetid, originalassetid, contentstreamid, nodeaggregateid, origindimensionspacepoint, origindimensionspacepointhash, propertyname)' - . ' SELECT assetid, originalassetid, :targetContentStreamId AS contentstreamid,' - . ' nodeaggregateid, origindimensionspacepoint, origindimensionspacepointhash, propertyname' - . ' FROM ' . $this->tableNamePrefix - . ' WHERE contentStreamId = :sourceContentStreamId', - [ - 'sourceContentStreamId' => $sourceContentStreamId->value, - 'targetContentStreamId' => $targetContentStreamId->value, - ] - ); - } - - public function copyDimensions( - OriginDimensionSpacePoint $sourceOriginDimensionSpacePoint, - OriginDimensionSpacePoint $targetOriginDimensionSpacePoint, - ): void { - try { - $this->dbal->executeStatement( - 'INSERT INTO ' . $this->tableNamePrefix . ' (assetid, originalassetid, contentstreamid, nodeaggregateid, origindimensionspacepoint, origindimensionspacepointhash, propertyname)' - . ' SELECT assetid, originalassetid, contentstreamid, nodeaggregateid,' - . ' :targetOriginDimensionSpacePoint AS origindimensionspacepoint,' - . ' :targetOriginDimensionSpacePointHash AS origindimensionspacepointhash, propertyname' - . ' FROM ' . $this->tableNamePrefix - . ' WHERE originDimensionSpacePointHash = :sourceOriginDimensionSpacePointHash', - [ - 'sourceOriginDimensionSpacePointHash' => $sourceOriginDimensionSpacePoint->hash, - 'targetOriginDimensionSpacePoint' => $targetOriginDimensionSpacePoint->toJson(), - 'targetOriginDimensionSpacePointHash' => $targetOriginDimensionSpacePoint->hash, - ] - ); - } catch (UniqueConstraintViolationException $e) { - // A usage already exists for this node and property -> can be ignored - } - } - - public function remove(AssetUsage $usage): void - { - $this->dbal->delete($this->tableNamePrefix, [ - 'assetId' => $usage->assetId, - 'contentStreamId' => $usage->contentStreamId->value, - 'nodeAggregateId' => $usage->nodeAggregateId->value, - 'originDimensionSpacePointHash' => $usage->originDimensionSpacePoint->hash, - 'propertyName' => $usage->propertyName, - ]); - } - - public function removeAsset(string $assetId): void - { - $this->dbal->delete($this->tableNamePrefix, [ - 'assetId' => $assetId, - ]); - } - - public function removeNode( - NodeAggregateId $nodeAggregateId, - DimensionSpacePointSet $dimensionSpacePoints, - ): void { - $this->dbal->executeStatement( - 'DELETE FROM ' . $this->tableNamePrefix - . ' WHERE nodeAggregateId = :nodeAggregateId' - . ' AND originDimensionSpacePointHash IN (:dimensionSpacePointHashes)', - [ - 'nodeAggregateId' => $nodeAggregateId->value, - 'dimensionSpacePointHashes' => $dimensionSpacePoints->getPointHashes(), - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, - ] - ); - } - - /** - * @throws DBALException - */ - public function reset(): void - { - /** @var AbstractPlatform|null $platform */ - $platform = $this->dbal->getDatabasePlatform(); - if ($platform === null) { - throw new \RuntimeException( - sprintf( - 'Failed to determine database platform for database "%s"', - $this->dbal->getDatabase() - ), - 1645781464 - ); - } - $this->dbal->executeStatement($platform->getTruncateTableSQL($this->tableNamePrefix)); - } - - public function getTableNamePrefix(): string - { - return $this->tableNamePrefix; - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepositoryFactory.php b/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepositoryFactory.php deleted file mode 100644 index f930887a523..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Projection/AssetUsageRepositoryFactory.php +++ /dev/null @@ -1,27 +0,0 @@ -dbal, - sprintf('cr_%s_p_neos_%s', $contentRepositoryId->value, 'asset_usage') - ); - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php new file mode 100644 index 00000000000..56c8aa7bff4 --- /dev/null +++ b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php @@ -0,0 +1,338 @@ + */ + private array $originalAssetIdMappingRuntimeCache = []; + + public function __construct( + private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly AssetUsageRepository $assetUsageRepository, + private readonly AssetRepository $assetRepository, + private readonly PersistenceManager $persistenceManager, + ) { + } + + /** @var array> */ + private array $workspaceBases = []; + + /** @var array> */ + private array $workspaceDependents = []; + + public function updateIndex(ContentRepositoryId $contentRepositoryId, Node $node): void + { + $workspaceBases = $this->getWorkspaceBasesAndWorkspace($contentRepositoryId, $node->workspaceName); + $workspaceDependents = $this->getWorkspaceDependents($contentRepositoryId, $node->workspaceName); + $nodeType = $this->contentRepositoryRegistry->get($contentRepositoryId)->getNodeTypeManager()->getNodeType($node->nodeTypeName); + + if ($nodeType === null) { + return; + } + + // 1. Get all asset usages of given node. + $assetIdsByPropertyOfNode = $this->getAssetIdsByProperty($nodeType, $node->properties); + + // 2. Get all existing asset usages of ancestor workspaces. + $assetUsagesInAncestorWorkspaces = $this->assetUsageRepository->findUsageForNodeInWorkspaces($contentRepositoryId, $node, $workspaceBases); + + // 3a. Filter only asset usages of given node, which are NOT already in place in ancestor workspaces. This way we get new asset usages in this particular workspace. + $propertiesAndAssetIdsNotExistingInAncestors = []; + foreach ($assetIdsByPropertyOfNode as $propertyName => $assetIdAndOriginalAssetIds) { + foreach ($assetIdAndOriginalAssetIds as $assetIdAndOriginalAssetId) { + foreach ($assetUsagesInAncestorWorkspaces as $assetUsage) { + if ( + $assetUsage->assetId === $assetIdAndOriginalAssetId->assetId + && $assetUsage->propertyName === $propertyName + ) { + // Found the asset usage in at least one ancestor workspace + continue 2; + } + } + $propertiesAndAssetIdsNotExistingInAncestors[$propertyName][] = $assetIdAndOriginalAssetId; + } + } + $assetIdsByPropertyNotExistingInAncestors = new AssetIdsByProperty($propertiesAndAssetIdsNotExistingInAncestors); + + // 3b. Filter all asset usages, which are existing in ancestor workspaces, but not in current workspace anymore (e.g. asset removed from property). + $removedPropertiesAndAssetIds = []; + foreach ($assetUsagesInAncestorWorkspaces as $assetUsage) { + $assetUsageFound = false; + $removedAssetIds = []; + foreach ($assetIdsByPropertyOfNode as $property => $assetIdAndOriginalAssetIds) { + if ($assetUsage->propertyName === $property) { + foreach ($assetIdAndOriginalAssetIds as $assetIdAndOriginalAssetId) { + if ( + $assetUsage->assetId === $assetIdAndOriginalAssetId->assetId + ) { + $assetUsageFound = true; + continue 2; + } + } + // No matching asset usage for the property found in the given node, but are existing in the ancestor workspaces. + $removedAssetIds[] = $assetUsage->assetId; + } + } + // No asset usage for the property found in the given node, but are existing in the ancestor workspaces. + if (!$assetUsageFound) { + $removedAssetIds[] = $assetUsage->assetId; + } + $removedPropertiesAndAssetIds[$assetUsage->propertyName] = array_map( + fn ($removedAssetIds) => new AssetIdAndOriginalAssetId($removedAssetIds, $this->findOriginalAssetId($removedAssetIds)), + $removedAssetIds + ); + } + $removedAssetIdsByProperty = new AssetIdsByProperty($removedPropertiesAndAssetIds); + + // 4. Actual execution to the index + // 4a. Handle new asset usages + foreach ($assetIdsByPropertyNotExistingInAncestors as $propertyName => $assetIdAndOriginalAssetIds) { + /** @var AssetIdAndOriginalAssetId $assetIdAndOriginalAssetId */ + foreach ($assetIdAndOriginalAssetIds as $assetIdAndOriginalAssetId) { + // Add usage to current workspace. + $this->assetUsageRepository->addUsagesForNodeWithAssetOnProperty($contentRepositoryId, $node, $propertyName, $assetIdAndOriginalAssetId->assetId, $assetIdAndOriginalAssetId->originalAssetId); + // Cleanup: Remove asset usage on all dependent workspaces. + $this->assetUsageRepository->removeAssetUsagesForNodeAggregateIdAndDimensionSpacePointWithAssetOnPropertyInWorkspaces( + $contentRepositoryId, + $node->aggregateId, + $node->dimensionSpacePoint, + $propertyName, + $assetIdAndOriginalAssetId->assetId, + $workspaceDependents + ); + // => During publish the asset usage moves from dependent workspace to base workspace. + } + } + // 4b. Handle removed asset usages + foreach ($removedAssetIdsByProperty as $propertyName => $assetIdAndOriginalAssetIds) { + /** @var AssetIdAndOriginalAssetId $assetIdAndOriginalAssetId */ + foreach ($assetIdAndOriginalAssetIds as $assetIdAndOriginalAssetId) { + $this->assetUsageRepository->removeAssetUsagesForNodeAggregateIdAndDimensionSpacePointWithAssetOnPropertyInWorkspaces( + $contentRepositoryId, + $node->aggregateId, + $node->dimensionSpacePoint, + $propertyName, + $assetIdAndOriginalAssetId->assetId, + [$node->workspaceName] + ); + } + } + } + + public function updateDimensionSpacePointInIndex(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, DimensionSpacePoint $source, DimensionSpacePoint $target): void + { + $this->assetUsageRepository->updateAssetUsageDimensionSpacePoint($contentRepositoryId, $workspaceName, $source, $target); + } + + public function removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( + ContentRepositoryId $contentRepositoryId, + WorkspaceName $workspaceName, + NodeAggregateId $nodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint + ): void { + $this->assetUsageRepository->removeAssetUsagesOfWorkspaceWithAllProperties( + $contentRepositoryId, + $workspaceName, + $nodeAggregateId, + $dimensionSpacePoint + ); + } + + public function removeIndexForNode( + ContentRepositoryId $contentRepositoryId, + Node $node + ): void { + $this->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( + $contentRepositoryId, + $node->workspaceName, + $node->aggregateId, + $node->dimensionSpacePoint + ); + } + + public function removeIndexForWorkspace( + ContentRepositoryId $contentRepositoryId, + WorkspaceName $workspaceName + ): void { + $this->assetUsageRepository->removeAssetUsagesOfWorkspace($contentRepositoryId, $workspaceName); + } + + public function pruneIndex(ContentRepositoryId $contentRepositoryId): void + { + $this->assetUsageRepository->removeAll($contentRepositoryId); + } + + /** + * @return WorkspaceName[] + */ + private function getWorkspaceBasesAndWorkspace(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): array + { + if (!isset($this->workspaceBases[$contentRepositoryId->value][$workspaceName->value])) { + $workspaceFinder = $this->contentRepositoryRegistry->get($contentRepositoryId)->getWorkspaceFinder(); + $workspace = $workspaceFinder->findOneByName($workspaceName); + if ($workspace === null) { + throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); + } + + $stack = [$workspace]; + + $collectedWorkspaceNames = [$workspaceName]; + + while ($stack !== []) { + $workspace = array_shift($stack); + if ($workspace->baseWorkspaceName) { + $ancestor = $workspaceFinder->findOneByName($workspace->baseWorkspaceName); + if ($ancestor === null) { + throw WorkspaceDoesNotExist::butWasSupposedTo($workspace->baseWorkspaceName); + } + $stack[] = $ancestor; + $collectedWorkspaceNames[] = $ancestor->workspaceName; + } + } + + $this->workspaceBases[$contentRepositoryId->value][$workspaceName->value] = $collectedWorkspaceNames; + } + + return $this->workspaceBases[$contentRepositoryId->value][$workspaceName->value]; + } + + /** + * @return WorkspaceName[] + */ + private function getWorkspaceDependents(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): array + { + if (!isset($this->workspaceDependents[$contentRepositoryId->value][$workspaceName->value])) { + $workspaceFinder = $this->contentRepositoryRegistry->get($contentRepositoryId)->getWorkspaceFinder(); + $workspace = $workspaceFinder->findOneByName($workspaceName); + if ($workspace === null) { + throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); + } + $stack = [$workspace]; + $collectedWorkspaceNames = []; + + while ($stack !== []) { + /** @var Workspace $workspace */ + $workspace = array_shift($stack); + $descendants = $workspaceFinder->findByBaseWorkspace($workspace->workspaceName); + foreach ($descendants as $descendant) { + $collectedWorkspaceNames[] = $descendant->workspaceName; + $stack[] = $descendant; + } + } + $this->workspaceDependents[$contentRepositoryId->value][$workspaceName->value] = $collectedWorkspaceNames; + } + + return $this->workspaceDependents[$contentRepositoryId->value][$workspaceName->value]; + } + + private function getAssetIdsByProperty(NodeType $nodeType, PropertyCollection $propertyValues): AssetIdsByProperty + { + /** @var array> $assetIds */ + $assetIds = []; + foreach ($propertyValues->serialized() as $propertyName => $propertyValue) { + if (!$nodeType->hasProperty($propertyName)) { + continue; + } + $propertyType = $nodeType->getPropertyType($propertyName); + + try { + $extractedAssetIds = $this->extractAssetIds( + $propertyType, + $propertyValues->offsetGet($propertyName), + ); + } catch (\Exception) { + $extractedAssetIds = []; + // We can't deserialize the property, so skip. + } + + $assetIds[$propertyName] = array_map( + fn ($assetId) => new AssetIdAndOriginalAssetId($assetId, $this->findOriginalAssetId($assetId)), + $extractedAssetIds + ); + } + return new AssetIdsByProperty($assetIds); + } + + /** + * @return array + */ + private function extractAssetIds(string $type, mixed $value): array + { + if (is_string($value)) { + preg_match_all('/asset:\/\/(?[\w-]*)/i', $value, $matches, PREG_SET_ORDER); + return array_map(static fn (array $match) => $match['assetId'], $matches); + } + if (is_subclass_of($type, ResourceBasedInterface::class)) { + return [$this->persistenceManager->getIdentifierByObject($value)]; + } + + // Collection type? + /** @var array{type: string, elementType: string|null, nullable: bool} $parsedType */ + $parsedType = TypeHandling::parseType($type); + if ($parsedType['elementType'] === null) { + return []; + } + if ( + !is_subclass_of($parsedType['elementType'], ResourceBasedInterface::class) + && !is_subclass_of($parsedType['elementType'], \Stringable::class) + ) { + return []; + } + /** @var array> $assetIds */ + $assetIds = []; + /** @var iterable $value */ + foreach ($value as $elementValue) { + $assetIds[] = $this->extractAssetIds($parsedType['elementType'], $elementValue); + } + return array_merge(...$assetIds); + } + + private function findOriginalAssetId(string $assetId): ?string + { + if (!array_key_exists($assetId, $this->originalAssetIdMappingRuntimeCache)) { + try { + /** @var AssetInterface|null $asset */ + $asset = $this->assetRepository->findByIdentifier($assetId); + } /** @noinspection PhpRedundantCatchClauseInspection */ catch (ORMException) { + return null; + } + /** @phpstan-ignore-next-line */ + $this->originalAssetIdMappingRuntimeCache[$assetId] = $asset instanceof AssetVariantInterface ? $asset->getOriginalAsset()->getIdentifier() : null; + } + + return $this->originalAssetIdMappingRuntimeCache[$assetId]; + } +} diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php deleted file mode 100644 index f73bf9865d5..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ - private array $existingAssetsById = []; - - public function __construct( - private readonly ContentRepository $contentRepository, - private readonly AssetUsageFinder $assetUsageFinder, - private readonly AssetRepository $assetRepository, - private readonly AssetUsageRepository $assetUsageRepository, - ) { - } - - public function findAllUsages(): AssetUsages - { - return $this->assetUsageFinder->findByFilter(AssetUsageFilter::create()); - } - - public function removeAssetUsage(AssetUsage $assetUsage): void - { - $this->assetUsageRepository->remove($assetUsage); - } - - public function isAssetUsageStillValid(AssetUsage $usage): bool - { - if (!isset($this->existingAssetsById[$usage->assetId])) { - /** @var AssetInterface|null $asset */ - $asset = $this->assetRepository->findByIdentifier($usage->assetId); - $this->existingAssetsById[$usage->assetId] = $asset !== null; - } - if ($this->existingAssetsById[$usage->assetId] === false) { - return false; - } - $dimensionSpacePoint = $usage->originDimensionSpacePoint->toDimensionSpacePoint(); - - // FIXME: AssetUsage->workspaceName ? - $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->contentStreamId); - if (is_null($workspace)) { - return false; - } - $subGraph = $this->contentRepository->getContentGraph($workspace->workspaceName)->getSubgraph( - $dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - $node = $subGraph->findNodeById($usage->nodeAggregateId); - return $node !== null; - } -} diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php deleted file mode 100644 index 2e798ecf25d..00000000000 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @internal - */ -class AssetUsageSyncServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct( - private readonly AssetRepository $assetRepository, - private readonly AssetUsageRepositoryFactory $assetUsageRepositoryFactory, - ) { - } - - public function build( - ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies, - ): AssetUsageSyncService { - return new AssetUsageSyncService( - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->contentRepository->projectionState(AssetUsageFinder::class), - $this->assetRepository, - $this->assetUsageRepositoryFactory->build($serviceFactoryDependencies->contentRepositoryId), - ); - } -} diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index af2719e83e4..ba7bb7e9b27 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -21,7 +21,7 @@ use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; +use Neos\Neos\AssetUsage\AssetUsageService; use Neos\Utility\Files; class CrCommandController extends CommandController @@ -39,6 +39,7 @@ public function __construct( private readonly PersistenceManagerInterface $persistenceManager, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, + private readonly AssetUsageService $assetUsageService, ) { parent::__construct(); } @@ -65,7 +66,7 @@ public function exportCommand(string $path, string $contentRepository = 'default $filesystem, $contentRepository->getWorkspaceFinder(), $this->assetRepository, - $contentRepository->projectionState(AssetUsageFinder::class), + $this->assetUsageService, ) ); assert($exportService instanceof ExportService); diff --git a/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php index e17c2b685b4..da3a7c05c55 100644 --- a/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php +++ b/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php @@ -19,6 +19,9 @@ class AssetChangeHandlerForCacheFlushing { + /** @var array > */ + private array $workspaceRuntimeCache = []; + public function __construct( protected readonly GlobalAssetUsageService $globalAssetUsageService, protected readonly ContentRepositoryRegistry $contentRepositoryRegistry, @@ -42,37 +45,31 @@ public function registerAssetChange(AssetInterface $asset): void $filter = AssetUsageFilter::create() ->withAsset($this->persistenceManager->getIdentifierByObject($asset)) + ->groupByWorkspaceName() + ->groupByNodeAggregate() ->includeVariantsOfAsset(); - $workspaceNamesByContentStreamId = []; foreach ($this->globalAssetUsageService->findByFilter($filter) as $contentRepositoryId => $usages) { $contentRepository = $this->contentRepositoryRegistry->get(ContentRepositoryId::fromString($contentRepositoryId)); + foreach ($usages as $usage) { - // TODO: Remove when WorkspaceName is part of the AssetUsageProjection - $workspaceName = $workspaceNamesByContentStreamId[$contentRepositoryId][$usage->contentStreamId->value] ?? null; - if ($workspaceName === null) { - $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->contentStreamId); - if ($workspace === null) { + $workspaceNames = $this->getWorkspaceNameAndChildWorkspaceNames($contentRepository, $usage->workspaceName); + + foreach ($workspaceNames as $workspaceName) { + $nodeAggregate = $contentRepository->getContentGraph($workspaceName)->findNodeAggregateById($usage->nodeAggregateId); + if ($nodeAggregate === null) { continue; } - $workspaceName = $workspace->workspaceName; - $workspaceNamesByContentStreamId[$contentRepositoryId][$usage->contentStreamId->value] = $workspaceName; - } - // + $flushNodeAggregateRequest = FlushNodeAggregateRequest::create( + $contentRepository->id, + $workspaceName, + $nodeAggregate->nodeAggregateId, + $nodeAggregate->nodeTypeName, + $this->determineAncestorNodeAggregateIds($contentRepository, $workspaceName, $nodeAggregate->nodeAggregateId), + ); - $nodeAggregate = $contentRepository->getContentGraph($workspaceName)->findNodeAggregateById($usage->nodeAggregateId); - if ($nodeAggregate === null) { - continue; + $this->contentCacheFlusher->flushNodeAggregate($flushNodeAggregateRequest, CacheFlushingStrategy::ON_SHUTDOWN); } - $flushNodeAggregateRequest = FlushNodeAggregateRequest::create( - $contentRepository->id, - $workspaceName, - $nodeAggregate->nodeAggregateId, - $nodeAggregate->nodeTypeName, - $this->determineAncestorNodeAggregateIds($contentRepository, $workspaceName, $nodeAggregate->nodeAggregateId), - ); - - $this->contentCacheFlusher->flushNodeAggregate($flushNodeAggregateRequest, CacheFlushingStrategy::ON_SHUTDOWN); } } } @@ -95,4 +92,28 @@ private function determineAncestorNodeAggregateIds(ContentRepository $contentRep return NodeAggregateIds::fromArray($ancestorNodeAggregateIds); } + + /** + * @return WorkspaceName[] + */ + private function getWorkspaceNameAndChildWorkspaceNames(ContentRepository $contentRepository, WorkspaceName $workspaceName): array + { + if (!isset($this->workspaceRuntimeCache[$contentRepository->id->value][$workspaceName->value])) { + $workspaceNames = []; + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); + if ($workspace !== null) { + $stack[] = $workspace; + + while ($stack !== []) { + $workspace = array_shift($stack); + $workspaceNames[] = $workspace->workspaceName; + + $stack = array_merge($stack, array_values($contentRepository->getWorkspaceFinder()->findByBaseWorkspace($workspace->workspaceName))); + } + } + $this->workspaceRuntimeCache[$contentRepository->id->value][$workspaceName->value] = $workspaceNames; + } + + return $this->workspaceRuntimeCache[$contentRepository->id->value][$workspaceName->value]; + } } diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php index 321f6308afc..e3e8bcfe461 100644 --- a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php +++ b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php @@ -38,6 +38,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -159,7 +160,12 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even // cleared, leading to presumably duplicate nodes in the UI. || $eventInstance instanceof NodeAggregateWasMoved ) { - $contentGraph = $this->contentRepository->getContentGraph($eventInstance->workspaceName); + try { + $contentGraph = $this->contentRepository->getContentGraph($eventInstance->workspaceName); + } catch (WorkspaceDoesNotExist) { + return; + } + $nodeAggregate = $contentGraph->findNodeAggregateById( $eventInstance->getNodeAggregateId() ); @@ -197,9 +203,13 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event && $eventInstance instanceof EmbedsContentStreamId && $eventInstance instanceof EmbedsWorkspaceName ) { - $nodeAggregate = $this->contentRepository->getContentGraph($eventInstance->getWorkspaceName())->findNodeAggregateById( - $eventInstance->getNodeAggregateId() - ); + try { + $nodeAggregate = $this->contentRepository->getContentGraph($eventInstance->getWorkspaceName())->findNodeAggregateById( + $eventInstance->getNodeAggregateId() + ); + } catch (WorkspaceDoesNotExist) { + return; + } if ($nodeAggregate) { $this->scheduleCacheFlushJobForNodeAggregate( diff --git a/Neos.Neos/Classes/Service/ImageVariantGarbageCollector.php b/Neos.Neos/Classes/Service/ImageVariantGarbageCollector.php index e7b54a1cab1..8c6470d4e59 100644 --- a/Neos.Neos/Classes/Service/ImageVariantGarbageCollector.php +++ b/Neos.Neos/Classes/Service/ImageVariantGarbageCollector.php @@ -78,8 +78,7 @@ public function removeUnusedImageVariant(Node $node, $propertyName, $oldValue, $ // then we are safe to remove the asset here. if ( $usageItem instanceof AssetUsageReference - /** @phpstan-ignore-next-line todo needs repair see https://github.com/neos/neos-development-collection/issues/5145 */ - && $usageItem->getContentStreamId()->equals($node->subgraphIdentity->contentStreamId) + && $usageItem->getWorkspaceName()->equals($node->workspaceName) && $usageItem->getOriginDimensionSpacePoint()->equals($node->originDimensionSpacePoint) && $usageItem->getNodeAggregateId()->equals($node->aggregateId) ) { diff --git a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml index fe26202594d..a3ae6ab53c0 100644 --- a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml @@ -19,5 +19,5 @@ Neos: catchUpHooks: 'Neos.Neos:FlushContentCache': factoryObjectName: Neos\Neos\Fusion\Cache\GraphProjectorCatchUpHookForCacheFlushingFactory - 'Neos.Neos:AssetUsage': - factoryObjectName: Neos\Neos\AssetUsage\Projection\AssetUsageProjectionFactory + 'Neos.Neos:AssetUsage': + factoryObjectName: Neos\Neos\AssetUsage\CatchUpHook\AssetUsageCatchUpHookFactory diff --git a/Neos.Neos/Documentation/References/CommandReference.rst b/Neos.Neos/Documentation/References/CommandReference.rst index 442c6702a56..2d8a8e1f1c0 100644 --- a/Neos.Neos/Documentation/References/CommandReference.rst +++ b/Neos.Neos/Documentation/References/CommandReference.rst @@ -19,7 +19,7 @@ commands that may be available, use:: ./flow help -The following reference was automatically generated from code on 2024-09-26 +The following reference was automatically generated from code on 2024-10-01 .. _`Neos Command Reference: NEOS.FLOW`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst index 0217289419d..a95f2132dc7 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst @@ -3,7 +3,7 @@ FluidAdaptor ViewHelper Reference ################################# -This reference was automatically generated from code on 2024-09-26 +This reference was automatically generated from code on 2024-10-01 .. _`FluidAdaptor ViewHelper Reference: f:debug`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst index 3e1d91b0c12..74f02777c01 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst @@ -3,7 +3,7 @@ Form ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-09-26 +This reference was automatically generated from code on 2024-10-01 .. _`Form ViewHelper Reference: neos.form:form`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst index 5e70b8810d3..44666afc031 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst @@ -3,7 +3,7 @@ Media ViewHelper Reference ########################## -This reference was automatically generated from code on 2024-09-26 +This reference was automatically generated from code on 2024-10-01 .. _`Media ViewHelper Reference: neos.media:fileTypeIcon`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst index f381fd96079..86448b390eb 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst @@ -3,7 +3,7 @@ Neos ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-09-26 +This reference was automatically generated from code on 2024-10-01 .. _`Neos ViewHelper Reference: neos:backend.authenticationProviderLabel`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst index 2c8d86c5054..411d770af5d 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst @@ -3,7 +3,7 @@ TYPO3 Fluid ViewHelper Reference ################################ -This reference was automatically generated from code on 2024-09-26 +This reference was automatically generated from code on 2024-10-01 .. _`TYPO3 Fluid ViewHelper Reference: f:alias`: diff --git a/Neos.Neos/Migrations/Mysql/Version20240906102606.php b/Neos.Neos/Migrations/Mysql/Version20240906102606.php new file mode 100644 index 00000000000..a442cac274d --- /dev/null +++ b/Neos.Neos/Migrations/Mysql/Version20240906102606.php @@ -0,0 +1,56 @@ +abortIf( + !$this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform, + "Migration can only be executed safely on '\Doctrine\DBAL\Platforms\AbstractMySQLPlatform'." + ); + + $sql = <<addSql($sql); + } + + public function down(Schema $schema): void + { + $this->abortIf( + !$this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform, + "Migration can only be executed safely on '\Doctrine\DBAL\Platforms\AbstractMySQLPlatform'." + ); + + $this->addSql('DROP TABLE IF EXISTS `neos_asset_usage`'); + } +} diff --git a/Neos.Neos/Resources/Private/Partials/Module/Shared/DocumentBreadcrumb.html b/Neos.Neos/Resources/Private/Partials/Module/Shared/DocumentBreadcrumb.html index b467d49ebdd..b52da74d4ba 100644 --- a/Neos.Neos/Resources/Private/Partials/Module/Shared/DocumentBreadcrumb.html +++ b/Neos.Neos/Resources/Private/Partials/Module/Shared/DocumentBreadcrumb.html @@ -1 +1 @@ -{namespace neos=Neos\Neos\ViewHelpers}/ {documentNode.label} +{namespace neos=Neos\Neos\ViewHelpers}/ {neos:node.label(node: documentNode)} diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature new file mode 100644 index 00000000000..276437c37de --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -0,0 +1,78 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node aggregate with node without dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Scenario: Nodes on live workspace have been created + Given I am in workspace "live" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | live | {} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + + Scenario: Nodes on user workspace have been created + Given I am in workspace "user-workspace" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/02-CreateNodeAggregateWithNode_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/02-CreateNodeAggregateWithNode_WithDimensions.feature new file mode 100644 index 00000000000..bd793a4e12d --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/02-CreateNodeAggregateWithNode_WithDimensions.feature @@ -0,0 +1,89 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node aggregate with node with dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {"language": "de"} + + Scenario: Nodes on live workspace have been created + Given I am in workspace "live" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language":"de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language":"de"} | + | asset-1 | sir-nodeward-nodington-iii | asset | live | {"language":"fr"} | + + Scenario: Nodes on user workspace have been created + Given I am in workspace "user-workspace" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language":"de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language":"de"} | + | asset-1 | sir-nodeward-nodington-iii | asset | user-workspace | {"language":"fr"} | + diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/01-CreateNodeGeneralizationVariant.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/01-CreateNodeGeneralizationVariant.feature new file mode 100644 index 00000000000..7703848549c --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/01-CreateNodeGeneralizationVariant.feature @@ -0,0 +1,83 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node generalization variant + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"], "asset": "Asset:asset-1"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Scenario: Create node generalization variant of node with asset in property + When I am in workspace "user-workspace" and dimension space point {"language":"de"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "en"} | + | asset-1 | nody-mc-nodeface | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-workspace-cs-id" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "en"} | + | asset-1 | nody-mc-nodeface | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/02-CreateNodeSpecializationVariant.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/02-CreateNodeSpecializationVariant.feature new file mode 100644 index 00000000000..b32ef98d8ab --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/02-CreateNodeSpecializationVariant.feature @@ -0,0 +1,71 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node specialization variant + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"], "asset": "Asset:asset-1"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Scenario: Create node specialization variant of node with asset in property + When I am in workspace "user-workspace" and dimension space point {"language":"de"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | nody-mc-nodeface | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-1 | nody-mc-nodeface | asset | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/03-CreateNodeSpecializationVariant_InternalWorkspace.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/03-CreateNodeSpecializationVariant_InternalWorkspace.feature new file mode 100644 index 00000000000..c7c75163ea9 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/03-CreateNodeSpecializationVariant_InternalWorkspace.feature @@ -0,0 +1,80 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node peer variant with internal workspace between live and user workspace + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "internal-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "internal-cs-id" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "internal-workspace" | + | newContentStreamId | "user-cs-id" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"], "asset": "Asset:asset-1"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "internal-workspace" | + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Scenario: Create node peer variant of node with asset in property + When I am in workspace "user-workspace" and dimension space point {"language":"de"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | nody-mc-nodeface | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-1 | nody-mc-nodeface | asset | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/04-CreateNodePeerVariant.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/04-CreateNodePeerVariant.feature new file mode 100644 index 00000000000..da52b6839b3 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/02-NodeVariation/04-CreateNodePeerVariant.feature @@ -0,0 +1,70 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node peer variant + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"], "asset": "Asset:asset-1"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Scenario: Create node peer variant of node with asset in property + When I am in workspace "user-workspace" and dimension space point {"language":"de"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"fr"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "fr"} | + | asset-1 | nody-mc-nodeface | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/01-SetNodeProperties_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/01-SetNodeProperties_WithoutDimensions.feature new file mode 100644 index 00000000000..4bee209b5b3 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/01-SetNodeProperties_WithoutDimensions.feature @@ -0,0 +1,155 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node aggregate with node without dimensions + + Background: Create node aggregate with initial node + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + Then the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And I am in workspace "live" + And I am in dimension space point {} + + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2", "Asset:asset-3"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3"} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I am in workspace "user-workspace" + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in dimension space point {} + + Scenario: Set node properties without dimension and publish in user workspace + Given the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {} | + | propertyValues | {"asset": "Asset:asset-2"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | sir-david-nodenborough | asset-2 | asset | user-workspace | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Remove an asset from an existing property + Given the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {} | + | propertyValues | {"asset": null} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Remove an asset from an existing property from the live workspaces + Given I am in workspace "live" + And the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "live" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {} | + | propertyValues | {"asset": null} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Add an asset in a property + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | originDimensionSpacePoint | {} | + | propertyValues | {"asset": "Asset:asset-3"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | sir-nodeward-nodington-iii | asset-3 | asset | user-workspace | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Add new asset property to the assets array + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {} | + | propertyValues | {"assets": ["Asset:asset-1", "Asset:asset-2", "Asset:asset-3"]} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | nody-mc-nodeface | asset-1 | assets | user-workspace | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Removes an asset entry from an assets array (no user-workspace entry, as the removal doesn't get tracked intentionally) + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {} | + | propertyValues | {"assets": ["Asset:asset-3"]} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/02-SetNodeProperties_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/02-SetNodeProperties_WithDimensions.feature new file mode 100644 index 00000000000..dc11b39459a --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/03-NodeModification/02-SetNodeProperties_WithDimensions.feature @@ -0,0 +1,162 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Create node aggregate with node with dimensions + + Background: Create node aggregate with initial node + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + Then the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2", "Asset:asset-3"]} | + + And I am in dimension space point {"language": "fr"} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3"} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I am in workspace "user-workspace" + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in dimension space point {"language": "de"} + + Scenario: Set node properties without dimension and publish in user workspace + Given the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"asset": "Asset:asset-2"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {"language": "de"} | + | sir-david-nodenborough | asset-2 | asset | user-workspace | {"language": "de"} | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | + + Scenario: Remove an asset from an existing property + Given the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"asset": null} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {"language": "de"} | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | + + Scenario: Remove an asset from an existing property from the live workspaces + Given I am in workspace "live" + And the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "live" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"asset": null} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | + + Scenario: Add an asset in a property + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | originDimensionSpacePoint | {"language": "fr"} | + | propertyValues | {"asset": "Asset:asset-3"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | asset | user-workspace | {"language": "fr"} | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | + + Scenario: Add new asset property to the assets array + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"assets": ["Asset:asset-1", "Asset:asset-2", "Asset:asset-3"]} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {"language": "de"} | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-1 | assets | user-workspace | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | + + Scenario: Removes an asset entry from an assets array (no user-workspace entry, as the removal doesn't get tracked intentionally) + Given I am in workspace "user-workspace" + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"assets": ["Asset:asset-3"]} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {"language": "de"} | + | nody-mc-nodeface | asset-2 | assets | live | {"language": "de"} | + | nody-mc-nodeface | asset-3 | assets | live | {"language": "de"} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {"language": "fr"} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/04-NodeRemoval/01-RemoveNodeAggregate_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/04-NodeRemoval/01-RemoveNodeAggregate_WithoutDimensions.feature new file mode 100644 index 00000000000..9544a808a79 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/04-NodeRemoval/01-RemoveNodeAggregate_WithoutDimensions.feature @@ -0,0 +1,99 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Remove node aggregate with node without dimensions + + Background: Create node aggregate with initial node + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + Then the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And I am in workspace "live" + And I am in dimension space point {} + + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2", "Asset:asset-3"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3"} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I am in workspace "user-workspace" + + Then the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in dimension space point {} + + Scenario: Remove node aggregate in user-workspace + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allSpecializations" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | nody-mc-nodeface | asset-2 | assets | live | {} | + | nody-mc-nodeface | asset-3 | assets | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Remove node aggregate in live workspace + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | workspaceName | "live" | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allSpecializations" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-david-nodenborough | asset-1 | asset | live | {} | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | + + Scenario: Remove node aggregate with children in live workspace + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | workspaceName | "live" | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allSpecializations" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | nodeAggregateId | assetId | propertyName | workspaceName | originDimensionSpacePoint | + | sir-nodeward-nodington-iii | asset-3 | text | live | {} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/DimensionSpacePoints/01-MoveDimensionSpacePoints.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/DimensionSpacePoints/01-MoveDimensionSpacePoints.feature new file mode 100644 index 00000000000..db04d6f5765 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/DimensionSpacePoints/01-MoveDimensionSpacePoints.feature @@ -0,0 +1,187 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Move DimensionSpacePoints + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + Then the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And I am in workspace "user-workspace" + + Then the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"asset": "Asset:asset-2"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "fr"} | + + And I am in workspace "live" + + + Scenario: Rename a dimension value in live workspace + Given I change the content dimensions in content repository "default" to: + | Identifier | Values | Generalizations | + | language | de_DE,gsw,fr,en | gsw->de_DE->en, fr | + + And I run the following node migration for workspace "live", creating target workspace "migration-cs" on contentStreamId "migration-cs", with publishing on success: + """yaml + migration: + - + transformations: + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"de"} + to: {"language":"de_DE"} + """ + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de_DE"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de_DE"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "fr"} | + + + Scenario: Rename a dimension value in user workspace + Given I change the content dimensions in content repository "default" to: + | Identifier | Values | Generalizations | + | language | de_DE,gsw,fr,en | gsw->de_DE->en, fr | + + And I run the following node migration for workspace "user-workspace", creating target workspace "migration-cs" on contentStreamId "migration-cs", with publishing on success: + """yaml + migration: + - + transformations: + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"de"} + to: {"language":"de_DE"} + """ + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "de_DE"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "fr"} | + + + Scenario: Adding a dimension in live workspace + Given I change the content dimensions in content repository "default" to: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + | market | DE, FR | DE, FR | + + And I run the following node migration for workspace "live", creating target workspace "migration-cs" on contentStreamId "migration-cs", with publishing on success: + """yaml + migration: + - + transformations: + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"de"} + to: {"language":"de", "market": "DE"} + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"fr"} + to: {"language":"fr", "market": "FR"} + """ + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language":"de", "market": "DE"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language":"de", "market": "DE"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language":"fr", "market": "FR"} | + + + Scenario: Adding a dimension in user workspace + Given I change the content dimensions in content repository "default" to: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + | market | DE, FR | DE, FR | + + And I run the following node migration for workspace "user-workspace", creating target workspace "migration-cs" on contentStreamId "migration-cs", with publishing on success: + """yaml + migration: + - + transformations: + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"de"} + to: {"language":"de", "market": "DE"} + - + type: 'MoveDimensionSpacePoint' + settings: + from: {"language":"fr"} + to: {"language":"fr", "market": "FR"} + """ + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language":"de", "market": "DE"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "fr"} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/01-Indexing_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/01-Indexing_WithoutDimensions.feature new file mode 100644 index 00000000000..6918fc7104d --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/01-Indexing_WithoutDimensions.feature @@ -0,0 +1,87 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Build index for existing nodes without dimensions + + Scenario: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-ii | curador | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + When I am in workspace "user-workspace" + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iv | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + | sir-nodeward-nodington-v | quatilde | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {} | + | propertyValues | {"asset": "Asset:asset-2"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | live | {} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + | asset-2 | sir-nodeward-nodington-v | assets | user-workspace | {} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {} | + + When I run the AssetUsageIndexingProcessor with rootNodeTypeName "Neos.ContentRepository:Root" + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | live | {} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + | asset-2 | sir-nodeward-nodington-v | assets | user-workspace | {} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {} | + diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/02-Indexing_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/02-Indexing_WithDimensions.feature new file mode 100644 index 00000000000..ce66c4f7324 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/Indexing/02-Indexing_WithDimensions.feature @@ -0,0 +1,104 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Build index for existing nodes with dimensions + + Scenario: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr | gsw->de, fr | + | market | EU, DE | EU, DE->EU | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de", "market": "DE"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-ii | curador | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de", "market": "DE"} | + | targetOrigin | {"language":"gsw", "market": "EU"} | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + When I am in workspace "user-workspace" + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iv | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + | sir-nodeward-nodington-v | quatilde | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language": "gsw", "market": "EU"} | + | propertyValues | {"asset": "Asset:asset-2"} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de", "market": "DE"} | + | targetOrigin | {"language":"gsw", "market": "EU"} | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de", "market": "DE"} | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "gsw", "market": "EU"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de", "market": "DE"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de", "market": "DE"} | + | asset-2 | sir-nodeward-nodington-v | assets | user-workspace | {"language": "de", "market": "DE"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "gsw", "market": "EU"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw", "market": "EU"} | + + When I run the AssetUsageIndexingProcessor with rootNodeTypeName "Neos.ContentRepository:Root" + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de", "market": "DE"} | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "gsw", "market": "EU"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de", "market": "DE"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de", "market": "DE"} | + | asset-2 | sir-nodeward-nodington-v | assets | user-workspace | {"language": "de", "market": "DE"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "gsw", "market": "EU"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw", "market": "EU"} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/01-PublishWorkspace_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/01-PublishWorkspace_WithoutDimensions.feature new file mode 100644 index 00000000000..4b538399636 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/01-PublishWorkspace_WithoutDimensions.feature @@ -0,0 +1,121 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes without dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Scenario: Publish nodes from user workspace to live + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-cs-id" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | live | {} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + + Scenario: Publish nodes from user workspace to a non live workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + And I am in dimension space point {} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-workspace-cs-id" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | review-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | review-workspace | {} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/02-PublishWorkspace_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/02-PublishWorkspace_WithDimensions.feature new file mode 100644 index 00000000000..d2b09329dcf --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/02-PublishWorkspace_WithDimensions.feature @@ -0,0 +1,191 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes with dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Scenario: Publish nodes from user workspace to live + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-cs-id" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "fr"} | + + Scenario: Publish nodes from user workspace to a non live workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "gsw"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"gsw"} | + | propertyValues | {"assets": ["Asset:asset-2", "Asset:asset-1"], "text": "Some text"} | + + And I am in dimension space point {"language": "fr"} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-workspace-cs-id" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | review-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | review-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | review-workspace | {"language": "fr"} | + + Scenario: Publish nodes from user workspace to live with new generalization + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "de"} | + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | newContentStreamId | "new-user-cs-id" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | live | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/03-PublishIndividualNodesFromWorkspace_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/03-PublishIndividualNodesFromWorkspace_WithoutDimensions.feature new file mode 100644 index 00000000000..d7c95053274 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/03-PublishIndividualNodesFromWorkspace_WithoutDimensions.feature @@ -0,0 +1,123 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes partially without dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Scenario: Publish nodes partially from user workspace to live + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + When the command PublishIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | nodesToPublish | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | + | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | + | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + Scenario: Publish nodes partially from user workspace to a non live workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + And I am in dimension space point {} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + When the command PublishIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | nodesToPublish | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] | + | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | + | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | review-workspace | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/04-PublishIndividualNodesFromWorkspace_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/04-PublishIndividualNodesFromWorkspace_WithDimensions.feature new file mode 100644 index 00000000000..1d039f580be --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W01-WorkspacePublication/04-PublishIndividualNodesFromWorkspace_WithDimensions.feature @@ -0,0 +1,194 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes partially with dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Scenario: Publish nodes partially from user workspace to live + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command PublishIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | nodesToPublish | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-david-nodenborough"}] | + | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | + | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + Scenario: Publish nodes partially from user workspace to a non live workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "gsw"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"gsw"} | + | propertyValues | {"assets": ["Asset:asset-2", "Asset:asset-1"]} | + + And I am in dimension space point {"language": "fr"} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command PublishIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | nodesToPublish | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-david-nodenborough"}] | + | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | + | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | review-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + Scenario: Publish nodes partially from user workspace to live with new generalization + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "de"} | + + When the command PublishIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | nodesToPublish | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-david-nodenborough"},{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "sir-david-nodenborough"}] | + | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | + | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/01-DiscardWorkspace_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/01-DiscardWorkspace_WithoutDimensions.feature new file mode 100644 index 00000000000..63909ce8de8 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/01-DiscardWorkspace_WithoutDimensions.feature @@ -0,0 +1,125 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Discard workspace without dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + + + Scenario: Discard changes in user workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + + + Scenario: Discard changes in user workspace with a non-live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And I am in workspace "review-workspace" + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + And I am in dimension space point {} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/02-DiscardWorkspace_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/02-DiscardWorkspace_WithDimensions.feature new file mode 100644 index 00000000000..c3c6559a152 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/02-DiscardWorkspace_WithDimensions.feature @@ -0,0 +1,216 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Discard workspace with dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + + Scenario: Discard user workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + + Scenario: Discard user workspace with a non-live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "gsw"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"gsw"} | + | propertyValues | {"assets": ["Asset:asset-2", "Asset:asset-1"], "text": "Some text"} | + + And I am in dimension space point {"language": "fr"} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + + Scenario: Discard user workspace with new generalization + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "de"} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + + + Scenario: Discard user workspace after change to an existing asseet usage of a property + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"language":"de"} | + | propertyValues | {"asset": "Asset:asset-2", "assets": ["Asset:asset-2", "Asset:asset-1"]} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | + | asset-2 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | sir-david-nodenborough | assets | user-workspace | {"language": "de"} | + | asset-1 | sir-david-nodenborough | assets | user-workspace | {"language": "de"} | + + When the command DiscardWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/03-DiscardIndividualNodesFromWorkspace_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/03-DiscardIndividualNodesFromWorkspace_WithoutDimensions.feature new file mode 100644 index 00000000000..059182db7bb --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/03-DiscardIndividualNodesFromWorkspace_WithoutDimensions.feature @@ -0,0 +1,220 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes partially without dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + + Scenario: Discards nodes partially from user workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-2", "text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + | asset-2 | sir-nodeward-nodington-iiii | asset | user-workspace | {} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iiii"}] | + | newContentStreamId | "user-cs-identifier-new" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + Scenario: Discards multiple nodes partially from user workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + And I am in dimension space point {} + + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-2", "text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + | asset-2 | sir-nodeward-nodington-iiii | asset | user-workspace | {} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iii"},{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iiii"}] | + | newContentStreamId | "user-cs-identifier-new" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {} | + + Scenario: Discards nodes partially from user workspace with a non-live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And I am in workspace "review-workspace" + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + And I am in dimension space point {} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset", "asset": "Asset:asset-1"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + | asset-1 | sir-nodeward-nodington-iiii | asset | user-workspace | {} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iiii"}] | + | newContentStreamId | "user-cs-identifier-new" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + + Scenario: Discards multiple nodes partially from user workspace with a non-live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And I am in workspace "review-workspace" + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + And I am in dimension space point {} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset", "asset": "Asset:asset-1"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {} | + | asset-1 | sir-nodeward-nodington-iiii | asset | user-workspace | {} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iii"},{"workspaceName": "user-workspace", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-nodeward-nodington-iiii"}] | + | newContentStreamId | "user-cs-identifier-new" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | review-workspace | {} | + diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/04-DiscardIndividualNodesFromWorkspace_WithDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/04-DiscardIndividualNodesFromWorkspace_WithDimensions.feature new file mode 100644 index 00000000000..e6b4bbb2628 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/W02-WorkspaceDiscarding/04-DiscardIndividualNodesFromWorkspace_WithDimensions.feature @@ -0,0 +1,188 @@ +@contentrepository @adapters=DoctrineDBAL +@flowEntities +Feature: Publish nodes partially with dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de,gsw,fr,en | gsw->de->en, fr | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:NodeWithAssetProperties': + properties: + text: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + + And I am in workspace "live" + And I am in dimension space point {"language": "de"} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When an asset exists with id "asset-1" + And an asset exists with id "asset-2" + And an asset exists with id "asset-3" + + Scenario: Discards nodes partially from user workspace with live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "fr"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-david-nodenborough"}, {"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "nody-mc-nodeface"}] | + | newContentStreamId | "user-cs-identifier-new" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + Scenario: Discards nodes partially from user workspace with non live base workspace + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "review-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review-workspace" | + + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "review-workspace" | + | newContentStreamId | "user-workspace-cs-id" | + + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + And I am in workspace "user-workspace" + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + + Then I am in dimension space point {"language": "gsw"} + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"gsw"} | + | propertyValues | {"assets": ["Asset:asset-2", "Asset:asset-1"]} | + + And I am in dimension space point {"language": "fr"} + Then the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-1 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "gsw"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "gsw"}, "nodeAggregateId": "nody-mc-nodeface"}] | + | newContentStreamId | "user-cs-identifier-new" | + + Then I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "fr"} | + + Scenario: Discard nodes partially from user workspace with live base workspace with new generalization + Given the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And I am in workspace "user-workspace" + And the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + + Then I am in dimension space point {"language": "de"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-nodington-iiii | bakura | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Text Without Asset"} | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"en"} | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "en"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | + | asset-3 | sir-nodeward-nodington-iii | text | user-workspace | {"language": "de"} | + + When the command DiscardIndividualNodesFromWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-workspace" | + | nodesToDiscard | [{"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "en"} , "nodeAggregateId": "sir-david-nodenborough"}, {"workspaceName": "user-workspace", "dimensionSpacePoint": {"language": "de"} , "nodeAggregateId": "sir-nodeward-nodington-iii"}] | + | newContentStreamId | "user-cs-identifier-new" | + + And I expect the AssetUsageService to have the following AssetUsages: + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | user-workspace | {"language": "de"} | + | asset-2 | nody-mc-nodeface | assets | user-workspace | {"language": "de"} | \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetUsageTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetUsageTrait.php new file mode 100644 index 00000000000..53d5e61c49f --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetUsageTrait.php @@ -0,0 +1,78 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + /** + * @Then I expect the AssetUsageService to have the following AssetUsages: + */ + public function iExpectTheAssetUsageServiceToHaveTheFollowingAssetUsages(TableNode $table) + { + $assetUsageService = $this->getObject(AssetUsageService::class); + $assetUsages = $assetUsageService->findByFilter($this->currentContentRepository->id, AssetUsageFilter::create()); + + $tableRows = $table->getHash(); + foreach ($assetUsages as $assetUsage) { + foreach ($tableRows as $tableRowIndex => $tableRow) { + if ($assetUsage->assetId !== $tableRow['assetId'] + || $assetUsage->propertyName !== $tableRow['propertyName'] + || !$assetUsage->workspaceName->equals(WorkspaceName::fromString($tableRow['workspaceName'])) + || !$assetUsage->nodeAggregateId->equals(NodeAggregateId::fromString($tableRow['nodeAggregateId'])) + || !$assetUsage->originDimensionSpacePoint->equals(DimensionSpacePoint::fromJsonString($tableRow['originDimensionSpacePoint'])) + ) { + continue; + } + unset($tableRows[$tableRowIndex]); + continue 2; + } + } + + Assert::assertEmpty($tableRows, "Not all given asset usages where found."); + Assert::assertSame($assetUsages->count(), count($table->getHash()), "More asset usages found as given."); + + } + + /** + * @When I run the AssetUsageIndexingProcessor with rootNodeTypeName ":rootNodeTypeName" + */ + public function iRunTheAssetUsageIndexingProcessor(string $rootNodeTypeName) + { + $this->getObject(AssetUsageIndexingProcessor::class)->buildIndex( + $this->currentContentRepository, + NodeTypeName::fromString($rootNodeTypeName), + ); + } +} \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 08699ce17ca..71c94d72ba7 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -42,6 +42,7 @@ class FeatureContext implements BehatContext use FusionTrait; use ContentCacheTrait; + use AssetUsageTrait; use AssetTrait; use WorkspaceServiceTrait;