Skip to content

Commit dcf6dbe

Browse files
authored
Merge pull request #4093 from neos/90-migrating-content
9.0 FEATURE: Migrate content dimensions in a running installation
2 parents 678b03b + 2373b7b commit dcf6dbe

File tree

20 files changed

+1012
-31
lines changed

20 files changed

+1012
-31
lines changed

Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated;
4141
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated;
4242
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated;
43+
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated;
4344
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated;
4445
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
4546
use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface;
@@ -160,6 +161,7 @@ public function canHandle(Event $event): bool
160161
$eventClassName = $this->eventNormalizer->getEventClassName($event);
161162
return in_array($eventClassName, [
162163
RootNodeAggregateWithNodeWasCreated::class,
164+
RootNodeAggregateDimensionsWereUpdated::class,
163165
NodeAggregateWithNodeWasCreated::class,
164166
NodeAggregateNameWasChanged::class,
165167
ContentStreamWasForked::class,
@@ -204,6 +206,8 @@ private function apply(EventEnvelope $eventEnvelope, CatchUpHookInterface $catch
204206

205207
if ($eventInstance instanceof RootNodeAggregateWithNodeWasCreated) {
206208
$this->whenRootNodeAggregateWithNodeWasCreated($eventInstance);
209+
} elseif ($eventInstance instanceof RootNodeAggregateDimensionsWereUpdated) {
210+
$this->whenRootNodeAggregateDimensionsWereUpdated($eventInstance);
207211
} elseif ($eventInstance instanceof NodeAggregateWithNodeWasCreated) {
208212
$this->whenNodeAggregateWithNodeWasCreated($eventInstance);
209213
} elseif ($eventInstance instanceof NodeAggregateNameWasChanged) {
@@ -275,12 +279,12 @@ public function markStale(): void
275279
private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event): void
276280
{
277281
$nodeRelationAnchorPoint = NodeRelationAnchorPoint::create();
278-
$dimensionSpacePoint = DimensionSpacePoint::fromArray([]);
282+
$originDimensionSpacePoint = OriginDimensionSpacePoint::fromArray([]);
279283
$node = new NodeRecord(
280284
$nodeRelationAnchorPoint,
281285
$event->nodeAggregateId,
282-
$dimensionSpacePoint->coordinates,
283-
$dimensionSpacePoint->hash,
286+
$originDimensionSpacePoint->coordinates,
287+
$originDimensionSpacePoint->hash,
284288
SerializedPropertyValues::fromArray([]),
285289
$event->nodeTypeName,
286290
$event->nodeAggregateClassification
@@ -298,6 +302,47 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo
298302
});
299303
}
300304

305+
/**
306+
* @throws \Throwable
307+
*/
308+
private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDimensionsWereUpdated $event): void
309+
{
310+
$rootNodeAnchorPoint = $this->projectionContentGraph
311+
->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream(
312+
$event->nodeAggregateId,
313+
/** the origin DSP of the root node is always the empty dimension ({@see whenRootNodeAggregateWithNodeWasCreated}) */
314+
OriginDimensionSpacePoint::fromArray([]),
315+
$event->contentStreamId
316+
);
317+
if ($rootNodeAnchorPoint === null) {
318+
// should never happen.
319+
return;
320+
}
321+
322+
$this->transactional(function () use ($rootNodeAnchorPoint, $event) {
323+
// delete all hierarchy edges of the root node
324+
$this->getDatabaseConnection()->executeUpdate('
325+
DELETE FROM ' . $this->tableNamePrefix . '_hierarchyrelation
326+
WHERE
327+
parentnodeanchor = :parentNodeAnchor
328+
AND childnodeanchor = :childNodeAnchor
329+
AND contentstreamid = :contentStreamId
330+
', [
331+
'parentNodeAnchor' => (string)NodeRelationAnchorPoint::forRootEdge(),
332+
'childNodeAnchor' => (string)$rootNodeAnchorPoint,
333+
'contentStreamId' => (string)$event->contentStreamId
334+
]);
335+
// recreate hierarchy edges for the root node
336+
$this->connectHierarchy(
337+
$event->contentStreamId,
338+
NodeRelationAnchorPoint::forRootEdge(),
339+
$rootNodeAnchorPoint,
340+
$event->coveredDimensionSpacePoints,
341+
null
342+
);
343+
});
344+
}
345+
301346
/**
302347
* @throws \Throwable
303348
*/

Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint;
2121
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet;
2222
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches;
23+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter;
24+
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregates;
2325
use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException;
2426
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
2527
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
@@ -115,42 +117,50 @@ public function findNodeByIdAndOriginDimensionSpacePoint(
115117
) : null;
116118
}
117119

118-
/**
119-
* @throws \Exception
120-
*/
121120
public function findRootNodeAggregateByType(
122121
ContentStreamId $contentStreamId,
123122
NodeTypeName $nodeTypeName
124123
): NodeAggregate {
124+
return $this->findRootNodeAggregates(
125+
$contentStreamId,
126+
FindRootNodeAggregatesFilter::nodeTypeName($nodeTypeName)
127+
)->first();
128+
}
129+
130+
public function findRootNodeAggregates(
131+
ContentStreamId $contentStreamId,
132+
FindRootNodeAggregatesFilter $filter,
133+
): NodeAggregates {
125134
$connection = $this->client->getConnection();
126135

127136
$query = 'SELECT n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint
128137
FROM ' . $this->tableNamePrefix . '_node n
129138
JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h
130139
ON h.childnodeanchor = n.relationanchorpoint
131140
WHERE h.contentstreamid = :contentStreamId
132-
AND h.parentnodeanchor = :rootEdgeParentAnchorId
133-
AND n.nodetypename = :nodeTypeName';
141+
AND h.parentnodeanchor = :rootEdgeParentAnchorId ';
134142

135143
$parameters = [
136144
'contentStreamId' => (string)$contentStreamId,
137145
'rootEdgeParentAnchorId' => (string)NodeRelationAnchorPoint::forRootEdge(),
138-
'nodeTypeName' => (string)$nodeTypeName,
139146
];
140147

141-
$nodeRow = $connection->executeQuery($query, $parameters)->fetchAssociative();
142-
143-
if (!is_array($nodeRow)) {
144-
throw new \RuntimeException('Root Node Aggregate not found');
148+
if ($filter->nodeTypeName !== null) {
149+
$query .= ' AND n.nodetypename = :nodeTypeName';
150+
$parameters['nodeTypeName'] = (string)$filter->nodeTypeName;
145151
}
146152

147-
/** @var NodeAggregate $nodeAggregate The factory will return a NodeAggregate since the array is not empty */
148-
$nodeAggregate = $this->nodeFactory->mapNodeRowsToNodeAggregate(
149-
[$nodeRow],
153+
154+
$nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
155+
156+
157+
/** @var \Traversable<NodeAggregate> $nodeAggregates The factory will return a NodeAggregate since the array is not empty */
158+
$nodeAggregates = $this->nodeFactory->mapNodeRowsToNodeAggregates(
159+
$nodeRows,
150160
VisibilityConstraints::withoutRestrictions()
151161
);
152162

153-
return $nodeAggregate;
163+
return NodeAggregates::fromArray(iterator_to_array($nodeAggregates));
154164
}
155165

156166
public function findNodeAggregatesByType(

Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet;
2525
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
2626
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
27+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter;
28+
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregates;
2729
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
2830
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
2931
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
@@ -106,7 +108,17 @@ public function findRootNodeAggregateByType(
106108
ContentStreamId $contentStreamId,
107109
NodeTypeName $nodeTypeName
108110
): NodeAggregate {
109-
throw new \BadMethodCallException('method findRootNodeAggregateByType is not implemented yet.', 1645782874);
111+
return $this->findRootNodeAggregates(
112+
$contentStreamId,
113+
FindRootNodeAggregatesFilter::nodeTypeName($nodeTypeName)
114+
)->first();
115+
}
116+
117+
public function findRootNodeAggregates(
118+
ContentStreamId $contentStreamId,
119+
FindRootNodeAggregatesFilter $filter,
120+
): NodeAggregates {
121+
throw new \BadMethodCallException('method findRootNodeAggregates is not implemented yet.', 1645782874);
110122
}
111123

112124
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
@contentrepository @adapters=DoctrineDBAL
2+
Feature: Update Root Node aggregate dimensions
3+
4+
I want to update a root node aggregate's dimensions when the dimension config changes.
5+
6+
Background:
7+
Given I have the following content dimensions:
8+
| Identifier | Values | Generalizations |
9+
| language | mul, de | |
10+
And I have the following NodeTypes configuration:
11+
"""
12+
'Neos.ContentRepository:Root': []
13+
"""
14+
And I am user identified by "initiating-user-identifier"
15+
And the command CreateRootWorkspace is executed with payload:
16+
| Key | Value |
17+
| workspaceName | "live" |
18+
| workspaceTitle | "Live" |
19+
| workspaceDescription | "The live workspace" |
20+
| newContentStreamId | "cs-identifier" |
21+
And the graph projection is fully up to date
22+
And I am in content stream "cs-identifier"
23+
And the command CreateRootNodeAggregateWithNode is executed with payload:
24+
| Key | Value |
25+
| nodeAggregateId | "lady-eleonode-rootford" |
26+
| nodeTypeName | "Neos.ContentRepository:Root" |
27+
28+
29+
Scenario: Initial setup of the root node (similar to 01/RootNodeCreation/03-...)
30+
Then I expect exactly 2 events to be published on stream "ContentStream:cs-identifier"
31+
And event at index 1 is of type "RootNodeAggregateWithNodeWasCreated" with payload:
32+
| Key | Expected |
33+
| contentStreamId | "cs-identifier" |
34+
| nodeAggregateId | "lady-eleonode-rootford" |
35+
| nodeTypeName | "Neos.ContentRepository:Root" |
36+
| coveredDimensionSpacePoints | [{"language":"mul"},{"language":"de"}] |
37+
| nodeAggregateClassification | "root" |
38+
And event metadata at index 1 is:
39+
| Key | Expected |
40+
41+
When the graph projection is fully up to date
42+
Then I expect the node aggregate "lady-eleonode-rootford" to exist
43+
And I expect this node aggregate to be classified as "root"
44+
And I expect this node aggregate to be of type "Neos.ContentRepository:Root"
45+
And I expect this node aggregate to be unnamed
46+
And I expect this node aggregate to occupy dimension space points [[]]
47+
And I expect this node aggregate to cover dimension space points [{"language":"mul"},{"language":"de"}]
48+
And I expect this node aggregate to disable dimension space points []
49+
And I expect this node aggregate to have no parent node aggregates
50+
And I expect this node aggregate to have no child node aggregates
51+
52+
And I expect the graph projection to consist of exactly 1 node
53+
And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph
54+
And I expect this node to be classified as "root"
55+
And I expect this node to be of type "Neos.ContentRepository:Root"
56+
And I expect this node to be unnamed
57+
And I expect this node to have no properties
58+
59+
When I am in dimension space point {"language":"mul"}
60+
Then I expect the subgraph projection to consist of exactly 1 node
61+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
62+
And I expect this node to be classified as "root"
63+
And I expect this node to have no parent node
64+
And I expect this node to have no child nodes
65+
And I expect this node to have no preceding siblings
66+
And I expect this node to have no succeeding siblings
67+
And I expect this node to have no references
68+
And I expect this node to not be referenced
69+
70+
When I am in dimension space point {"language":"de"}
71+
Then I expect the subgraph projection to consist of exactly 1 node
72+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
73+
74+
75+
Scenario: Adding a dimension and updating the root node works
76+
When the graph projection is fully up to date
77+
Given I have the following content dimensions:
78+
| Identifier | Values | Generalizations |
79+
| language | mul, de, en | |
80+
81+
# in "en", the root node does not exist.
82+
When I am in dimension space point {"language":"en"}
83+
Then I expect the subgraph projection to consist of exactly 0 nodes
84+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to no node
85+
86+
And the command UpdateRootNodeAggregateDimensions is executed with payload:
87+
| Key | Value |
88+
| nodeAggregateId | "lady-eleonode-rootford" |
89+
90+
Then I expect exactly 3 events to be published on stream "ContentStream:cs-identifier"
91+
# the updated dimension config is persisted in the event stream
92+
And event at index 2 is of type "RootNodeAggregateDimensionsWereUpdated" with payload:
93+
| Key | Expected |
94+
| contentStreamId | "cs-identifier" |
95+
| nodeAggregateId | "lady-eleonode-rootford" |
96+
| coveredDimensionSpacePoints | [{"language":"mul"},{"language":"de"},{"language":"en"}] |
97+
And event metadata at index 1 is:
98+
| Key | Expected |
99+
100+
When the graph projection is fully up to date
101+
Then I expect the node aggregate "lady-eleonode-rootford" to exist
102+
And I expect this node aggregate to be classified as "root"
103+
And I expect this node aggregate to be of type "Neos.ContentRepository:Root"
104+
And I expect this node aggregate to be unnamed
105+
And I expect this node aggregate to occupy dimension space points [[]]
106+
And I expect this node aggregate to cover dimension space points [{"language":"mul"},{"language":"de"},{"language":"en"}]
107+
And I expect this node aggregate to disable dimension space points []
108+
And I expect this node aggregate to have no parent node aggregates
109+
And I expect this node aggregate to have no child node aggregates
110+
111+
And I expect the graph projection to consist of exactly 1 node
112+
And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph
113+
And I expect this node to be classified as "root"
114+
And I expect this node to be of type "Neos.ContentRepository:Root"
115+
And I expect this node to be unnamed
116+
And I expect this node to have no properties
117+
118+
When I am in dimension space point {"language":"mul"}
119+
Then I expect the subgraph projection to consist of exactly 1 node
120+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
121+
And I expect this node to be classified as "root"
122+
And I expect this node to have no parent node
123+
And I expect this node to have no child nodes
124+
And I expect this node to have no preceding siblings
125+
And I expect this node to have no succeeding siblings
126+
And I expect this node to have no references
127+
And I expect this node to not be referenced
128+
129+
When I am in dimension space point {"language":"de"}
130+
Then I expect the subgraph projection to consist of exactly 1 node
131+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
132+
133+
# now, the root node exists in "en"
134+
When I am in dimension space point {"language":"en"}
135+
Then I expect the subgraph projection to consist of exactly 1 node
136+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
137+
138+
139+
Scenario: Adding a dimension updating the root node, removing dimension, updating the root node, works (dimension gone again)
140+
When the graph projection is fully up to date
141+
Given I have the following content dimensions:
142+
| Identifier | Values | Generalizations |
143+
| language | mul, de, en | |
144+
And the command UpdateRootNodeAggregateDimensions is executed with payload:
145+
| Key | Value |
146+
| nodeAggregateId | "lady-eleonode-rootford" |
147+
And the graph projection is fully up to date
148+
149+
# now, the root node exists in "en"
150+
When I am in dimension space point {"language":"en"}
151+
Then I expect the subgraph projection to consist of exactly 1 nodes
152+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{}
153+
154+
# again, remove "en"
155+
Given I have the following content dimensions:
156+
| Identifier | Values | Generalizations |
157+
| language | mul, de, | |
158+
And the command UpdateRootNodeAggregateDimensions is executed with payload:
159+
| Key | Value |
160+
| nodeAggregateId | "lady-eleonode-rootford" |
161+
And the graph projection is fully up to date
162+
163+
# now, the root node should not exist anymore in "en"
164+
When I am in dimension space point {"language":"en"}
165+
Then I expect the subgraph projection to consist of exactly 0 nodes
166+
And I expect node aggregate identifier "lady-eleonode-rootford" to lead to no node

Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated;
2323
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated;
2424
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated;
25+
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated;
2526
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated;
2627
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated;
2728
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\WorkspaceWasCreated;
@@ -81,6 +82,7 @@ public function __construct()
8182
NodeSpecializationVariantWasCreated::class,
8283
RootNodeAggregateWithNodeWasCreated::class,
8384
RootWorkspaceWasCreated::class,
85+
RootNodeAggregateDimensionsWereUpdated::class,
8486
WorkspaceRebaseFailed::class,
8587
WorkspaceWasCreated::class,
8688
WorkspaceWasDiscarded::class,

0 commit comments

Comments
 (0)