Skip to content

Commit

Permalink
Merge pull request neos#4616 from neos/4609-multipleContentCollection…
Browse files Browse the repository at this point in the history
…Publishing

4609 multiple content collection publishing
  • Loading branch information
nezaniel authored Oct 18, 2023
2 parents fc37b92 + 58d60de commit 15b8d31
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@contentrepository @adapters=DoctrineDBAL
Feature: Individual node publication

Publishing an individual node works

Background:
Given using no content dimensions
And using the following node types:
"""yaml
'Neos.ContentRepository:Root': {}
'Neos.ContentRepository.Testing:Content': {}
'Neos.ContentRepository.Testing:Document':
childNodes:
child1:
type: 'Neos.ContentRepository.Testing:Content'
child2:
type: 'Neos.ContentRepository.Testing:Content'
"""
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" |
| newContentStreamId | "cs-identifier" |
And the graph projection is fully up to date
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "lady-eleonode-rootford" |
| nodeTypeName | "Neos.ContentRepository:Root" |

# Create user workspace
And the command CreateWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
| baseWorkspaceName | "live" |
| newContentStreamId | "user-cs-identifier" |
And the graph projection is fully up to date

################
# PUBLISHING
################
Scenario: It is possible to publish a single node; and only this one is live.
# create nodes in user WS
Given I am in content stream "user-cs-identifier" and dimension space point {}
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds |
| sir-david-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | document | {} |
And I remember NodeAggregateId of node "sir-david-nodenborough"s child "child2" as "child2Id"
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds |
| nody-mc-nodeface | Neos.ContentRepository.Testing:Content | $child2Id | nody | {} |
When the command PublishIndividualNodesFromWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
| nodesToPublish | [{"contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}, "nodeAggregateId": "sir-david-nodenborough"}] |
| contentStreamIdForRemainingPart | "user-cs-identifier-remaining" |
And the graph projection is fully up to date

And I am in content stream "cs-identifier"

Then I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths;
use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
Expand Down Expand Up @@ -89,6 +90,34 @@ public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPrope
);
}

/**
* Specify explicitly the node aggregate ids for the tethered children {@see tetheredDescendantNodeAggregateIds}.
*
* In case you want to create a batch of commands where one creates the node and a succeeding command needs
* a tethered node aggregate id, you need to generate the child node aggregate ids in advance.
*
* _Alternatively you would need to fetch the created tethered node first from the subgraph.
* {@see ContentSubgraphInterface::findChildNodeConnectedThroughEdgeName()}_
*
* The helper method {@see NodeAggregateIdsByNodePaths::createForNodeType()} will generate recursively
* node aggregate ids for every tethered child node:
*
* ```php
* $tetheredDescendantNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType(
* $command->nodeTypeName,
* $nodeTypeManager
* );
* $command = $command->withTetheredDescendantNodeAggregateIds($tetheredDescendantNodeAggregateIds):
* ```
*
* The generated node aggregate id for the tethered node "main" is this way known before the command is issued:
*
* ```php
* $mainNodeAggregateId = $command->tetheredDescendantNodeAggregateIds->getNodeAggregateId(NodePath::fromString('main'));
* ```
*
* Generating the node aggregate ids from user land is totally optional.
*/
public function withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths $tetheredDescendantNodeAggregateIds): self
{
return new self(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

namespace Neos\ContentRepository\Core\Feature\NodeCreation\Dto;

/*
* This file is part of the Neos.ContentRepository package.
*
Expand All @@ -12,6 +10,13 @@
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\NodeCreation\Dto;

use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;

Expand Down Expand Up @@ -56,6 +61,16 @@ public static function createEmpty(): self
return new self([]);
}

/**
* Generate the tethered node aggregate ids in advance
*
* {@see CreateNodeAggregateWithNode::withTetheredDescendantNodeAggregateIds}
*/
public static function createForNodeType(NodeTypeName $nodeTypeName, NodeTypeManager $nodeTypeManager): self
{
return self::fromArray(self::createNodeAggregateIdsForNodeType($nodeTypeName, $nodeTypeManager));
}

/**
* @param array<string,string|NodeAggregateId> $array
*/
Expand Down Expand Up @@ -95,7 +110,34 @@ public static function fromJsonString(string $jsonString): self

public function merge(self $other): self
{
return new self(array_merge($this->nodeAggregateIds, $other->getNodeAggregateIds()));
return new self(array_merge($this->nodeAggregateIds, $other->nodeAggregateIds));
}

public function completeForNodeOfType(NodeTypeName $nodeTypeName, NodeTypeManager $nodeTypeManager): self
{
return self::createForNodeType($nodeTypeName, $nodeTypeManager)
->merge($this);
}

/**
* @return array<string,NodeAggregateId>
*/
private static function createNodeAggregateIdsForNodeType(
NodeTypeName $nodeTypeName,
NodeTypeManager $nodeTypeManager,
?string $pathPrefix = null
): array {
$nodeAggregateIds = [];
foreach ($nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeTypeManager->getNodeType($nodeTypeName)) as $nodeName => $childNodeType) {
$path = $pathPrefix ? $pathPrefix . '/' . $nodeName : $nodeName;
$nodeAggregateIds[$path] = NodeAggregateId::create();
$nodeAggregateIds = array_merge(
$nodeAggregateIds,
self::createNodeAggregateIdsForNodeType($childNodeType->name, $nodeTypeManager, $path)
);
}

return $nodeAggregateIds;
}

public function getNodeAggregateId(NodePath $nodePath): ?NodeAggregateId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,9 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties(
$contentRepository
);
}
$descendantNodeAggregateIds = self::populateNodeAggregateIds(
$nodeType,
$this->getNodeTypeManager(),
$command->tetheredDescendantNodeAggregateIds
$descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType(
$command->nodeTypeName,
$this->nodeTypeManager
);
// Write the auto-created descendant node aggregate ids back to the command;
// so that when rebasing the command, it stays fully deterministic.
Expand Down Expand Up @@ -344,30 +343,4 @@ private function createTetheredWithNode(
$precedingNodeAggregateId
);
}

protected static function populateNodeAggregateIds(
NodeType $nodeType,
NodeTypeManager $nodeTypeManager,
?NodeAggregateIdsByNodePaths $nodeAggregateIds,
NodePath $childPath = null
): NodeAggregateIdsByNodePaths {
if ($nodeAggregateIds === null) {
$nodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty();
}
// TODO: handle Multiple levels of autocreated child nodes
foreach ($nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType) as $rawChildName => $childNodeType) {
$childName = NodeName::fromString($rawChildName);
$childPath = $childPath
? $childPath->appendPathSegment($childName)
: NodePath::fromString($childName->value);
if (!$nodeAggregateIds->getNodeAggregateId($childPath)) {
$nodeAggregateIds = $nodeAggregateIds->add(
$childPath,
NodeAggregateId::create()
);
}
}

return $nodeAggregateIds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use Neos\ContentRepository\Core\EventStore\EventsToPublish;
use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher;
use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName;
use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths;
use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved;
use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType;
use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged;
Expand All @@ -37,7 +36,6 @@
use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\ContentRepository\Core\SharedModel\User\UserId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\EventStore\Model\EventStream\ExpectedVersion;

Expand Down Expand Up @@ -90,13 +88,6 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid(
NodeType $nodeType
): bool;

abstract protected static function populateNodeAggregateIds(
NodeType $nodeType,
NodeTypeManager $nodeTypeManager,
NodeAggregateIdsByNodePaths $nodeAggregateIds,
NodePath $childPath = null
): NodeAggregateIdsByNodePaths;

abstract protected function createEventsForMissingTetheredNode(
NodeAggregate $parentNodeAggregate,
Node $parentNode,
Expand Down Expand Up @@ -160,10 +151,9 @@ private function handleChangeNodeAggregateType(
/**************
* Preparation - make the command fully deterministic in case of rebase
**************/
$descendantNodeAggregateIds = static::populateNodeAggregateIds(
$newNodeType,
$this->getNodeTypeManager(),
$command->tetheredDescendantNodeAggregateIds
$descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType(
$newNodeType->name,
$this->nodeTypeManager
);
// Write the auto-created descendant node aggregate ids back to the command;
// so that when rebasing the command, it stays fully deterministic.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/*
* This file is part of the Neos.ContentRepository.Core package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Tests\Unit\Feature\NodeCreation\Dto;

use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths;
use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use PHPUnit\Framework\TestCase;

/**
* Test cases for {@see NodeAggregateIdsByNodePaths}
*/
class NodeAggregateIdsByNodePathsTest extends TestCase
{
/**
* @param array<string,NodeAggregateId|null> $expectedNodeAggregateIdsByPath, null if the actual value is not important
* @dataProvider subjectProvider
*/
public function testCompleteForNodeOfType(NodeAggregateIdsByNodePaths $subject, array $expectedNodeAggregateIdsByPath): void
{
$nodeTypeManager = new NodeTypeManager(
fn (): array => [
'Neos.ContentRepository.Testing:Content' => [],
'Neos.ContentRepository.Testing:LeafDocument' => [
'childNodes' => [
'grandchild1' => [
'type' => 'Neos.ContentRepository.Testing:Content'
],
'grandchild2' => [
'type' => 'Neos.ContentRepository.Testing:Content'
]
]
],
'Neos.ContentRepository.Testing:Document' => [
'childNodes' => [
'child1' => [
'type' => 'Neos.ContentRepository.Testing:LeafDocument'
],
'child2' => [
'type' => 'Neos.ContentRepository.Testing:LeafDocument'
]
]
]
],
new DefaultNodeLabelGeneratorFactory()
);

$completeSubject = $subject->completeForNodeOfType(
NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'),
$nodeTypeManager
);

foreach ($expectedNodeAggregateIdsByPath as $path => $explicitExpectedNodeAggregateId) {
$actualNodeAggregateId = $completeSubject->getNodeAggregateId(NodePath::fromString($path));
self::assertInstanceOf(NodeAggregateId::class, $actualNodeAggregateId);
if ($explicitExpectedNodeAggregateId instanceof NodeAggregateId) {
self::assertTrue($actualNodeAggregateId->equals($explicitExpectedNodeAggregateId));
}
}
}

public static function subjectProvider(): iterable
{
yield 'emptySubject' => [
'subject' => NodeAggregateIdsByNodePaths::createEmpty(),
'expectedNodeAggregateIdsByPath' => [
'child1' => null,
'child2' => null,
'child1/grandchild1' => null,
'child1/grandchild2' => null,
'child2/grandchild1' => null,
'child2/grandchild2' => null,
]
];

yield 'alreadyCompleteSubject' => [
'subject' => NodeAggregateIdsByNodePaths::fromArray([
'child1' => NodeAggregateId::fromString('child-1'),
'child2' => NodeAggregateId::fromString('child-2'),
'child1/grandchild1' => NodeAggregateId::fromString('grandchild-1-1'),
'child1/grandchild2' => NodeAggregateId::fromString('grandchild-1-2'),
'child2/grandchild1' => NodeAggregateId::fromString('grandchild-2-1'),
'child2/grandchild2' => NodeAggregateId::fromString('grandchild-2-1'),
]),
'expectedNodeAggregateIdsByPath' => [
'child1' => NodeAggregateId::fromString('child-1'),
'child2' => NodeAggregateId::fromString('child-2'),
'child1/grandchild1' => NodeAggregateId::fromString('grandchild-1-1'),
'child1/grandchild2' => NodeAggregateId::fromString('grandchild-1-2'),
'child2/grandchild1' => NodeAggregateId::fromString('grandchild-2-1'),
'child2/grandchild2' => NodeAggregateId::fromString('grandchild-2-1'),
]
];
}
}
Loading

0 comments on commit 15b8d31

Please sign in to comment.