Skip to content

Commit

Permalink
FEATURE: References on creation and Copy
Browse files Browse the repository at this point in the history
This reworks references so that multiple reference properties can be
set via a single command and also references can be attached to
`CreateNodeAggregateWithNode` which is also used for copying nodes.
  • Loading branch information
kitsunet committed Aug 23, 2024
1 parent 2cdeb6a commit 1612659
Show file tree
Hide file tree
Showing 46 changed files with 634 additions and 512 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ Feature: Run integrity violation detection regarding reference relations
| Key | Value |
| sourceOriginDimensionSpacePoint | {"language":"de"} |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target": "anthony-destinode"}] |
| references | {"referenceProperty": [{"target": "anthony-destinode"}]} |
And I detach the following reference relation from its source:
| Key | Value |
| contentStreamId | "cs-identifier" |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues;
use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet;
use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceNameToEmpty;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReference;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet;
use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved;
use Neos\ContentRepository\Core\Feature\NodeRenaming\Event\NodeAggregateNameWasChanged;
Expand Down Expand Up @@ -434,6 +436,7 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre
$event->originDimensionSpacePoint,
$event->succeedingSiblingsForCoverage,
$event->initialPropertyValues,
$event->nodeReferences,
$event->nodeAggregateClassification,
$event->nodeName,
$eventEnvelope,
Expand Down Expand Up @@ -520,40 +523,52 @@ function (NodeRecord $node) use ($eventEnvelope) {
);

// remove old
try {
foreach ($event->references->getReferenceNames() as $referenceName) {
try {
$this->dbal->delete($this->tableNames->referenceRelation(), [
'nodeanchorpoint' => $nodeAnchorPoint?->value,
'name' => $event->referenceName->value
]);
} catch (DBALException $e) {
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to remove reference relation: %s', $e->getMessage()), 1716486309, $e);
}
}

// set new
$position = 0;
/** @var SerializedNodeReference $reference */
foreach ($event->references as $reference) {
$referencePropertiesJson = null;
if ($reference->properties !== null) {
try {
$referencePropertiesJson = \json_encode($reference->properties, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
} catch (\JsonException $e) {
throw new \RuntimeException(sprintf('Failed to JSON-encode reference properties: %s', $e->getMessage()), 1716486271, $e);
}
}
$nodeAnchorPoint && $this->writeReferencesForTargetAnchorPoint($event->references, $nodeAnchorPoint);
}
}

private function writeReferencesForTargetAnchorPoint(SerializedNodeReferences $nodeReferences, NodeRelationAnchorPoint $nodeAnchorPoint): void
{
$position = 0;
/** @var NodeReferenceNameToEmpty|SerializedNodeReference $reference */
foreach ($nodeReferences as $reference) {
if ($reference instanceof NodeReferenceNameToEmpty) {
// Reference empty happens separately
continue;
}

$referencePropertiesJson = null;
if ($reference->properties !== null && $reference->properties->count() > 0) {
try {
$this->dbal->insert($this->tableNames->referenceRelation(), [
'name' => $event->referenceName->value,
'position' => $position,
'nodeanchorpoint' => $nodeAnchorPoint?->value,
'destinationnodeaggregateid' => $reference->targetNodeAggregateId->value,
'properties' => $referencePropertiesJson,
]);
} catch (DBALException $e) {
throw new \RuntimeException(sprintf('Failed to insert reference relation: %s', $e->getMessage()), 1716486309, $e);
$referencePropertiesJson = \json_encode($reference->properties, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
} catch (\JsonException $e) {
throw new \RuntimeException(sprintf('Failed to JSON-encode reference properties: %s', $e->getMessage()), 1716486271, $e);
}
$position++;
}
try {
$this->dbal->insert($this->tableNames->referenceRelation(), [
'name' => $reference->referenceName->value,
'position' => $position,
'nodeanchorpoint' => $nodeAnchorPoint?->value,
'destinationnodeaggregateid' => $reference->targetNodeAggregateId->value,
'properties' => $referencePropertiesJson,
]);
} catch (DbalException $e) {
throw new \RuntimeException(sprintf('Failed to insert reference relation: %s', $e->getMessage()), 1716486309, $e);
}
$position++;
}
}

Expand Down Expand Up @@ -776,6 +791,7 @@ private function createNodeWithHierarchy(
OriginDimensionSpacePoint $originDimensionSpacePoint,
InterdimensionalSiblings $coverageSucceedingSiblings,
SerializedPropertyValues $propertyDefaultValuesAndTypes,
SerializedNodeReferences $references,
NodeAggregateClassification $nodeAggregateClassification,
?NodeName $nodeName,
EventEnvelope $eventEnvelope,
Expand Down Expand Up @@ -831,6 +847,8 @@ private function createNodeWithHierarchy(
}
}
}

$this->writeReferencesForTargetAnchorPoint($references, $node->relationAnchorPoint);
}

private function connectHierarchy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream;
use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated;
use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences;
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
Expand Down Expand Up @@ -142,6 +143,7 @@ private function createHierarchy(
null,
SerializedPropertyValues::createEmpty(),
NodeAggregateClassification::CLASSIFICATION_REGULAR,
SerializedNodeReferences::createEmpty(),
);
$sumSoFar++;
$this->createHierarchy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ Feature: Constraint checks on SetNodeReferences
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "ContentStreamIsClosed"

# checks for contentStreamId
Expand All @@ -73,25 +72,22 @@ Feature: Constraint checks on SetNodeReferences
| Key | Value |
| workspaceName | "i-do-not-exist" |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" with code 1513924741

# checks for sourceNodeAggregateId
Scenario: Try to reference nodes in a non-existent node aggregate
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "i-do-not-exist" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" with code 1541678486

Scenario: Try to reference nodes in a root node aggregate
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "lady-eleonode-rootford" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "NodeAggregateIsRoot"

# checks for sourceOriginDimensionSpacePoint
Expand All @@ -100,58 +96,51 @@ Feature: Constraint checks on SetNodeReferences
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| sourceOriginDimensionSpacePoint | {"undeclared":"undefined"} |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "DimensionSpacePointNotFound" with code 1505929456

Scenario: Try to reference nodes in an origin dimension space point the source node aggregate does not occupy
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| sourceOriginDimensionSpacePoint | {"language":"en"} |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "DimensionSpacePointIsNotYetOccupied" with code 1552595396

# checks for destinationnodeAggregateIds
Scenario: Try to reference a non-existent node aggregate
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"i-do-not-exist"}] |
| references | {"referenceProperty": [{"target":"i-do-not-exist"}]} |
Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" with code 1541678486

Scenario: Try to reference a root node aggregate
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"lady-eleonode-rootford"}] |
| references | {"referenceProperty": [{"target":"lady-eleonode-rootford"}]} |
Then the last command should have thrown an exception of type "NodeAggregateIsRoot"

Scenario: Try to set references exceeding the maxItems count
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "constrainedReferenceCount" |
| references | [{"target":"anthony-destinode"}, {"target":"berta-destinode"}] |
| references | {"constrainedReferenceCount": [{"target":"anthony-destinode"}, {"target":"berta-destinode"}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1700150156

Scenario: Try to set references exceeding the maxItems count for legacy property reference declaration
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referenceProperty" |
| references | [{"target":"anthony-destinode"}, {"target":"berta-destinode"}] |
| references | {"referenceProperty": [{"target":"anthony-destinode"}, {"target":"berta-destinode"}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1700150156

Scenario: Try to reference a node aggregate of a type not matching the constraints
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "constrainedReferenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"constrainedReferenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1648502149

Scenario: Try to reference a node aggregate which does not cover the source origin
Expand All @@ -166,49 +155,43 @@ Feature: Constraint checks on SetNodeReferences
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| sourceOriginDimensionSpacePoint | {"language": "de"} |
| referenceName | "referenceProperty" |
| references | [{"target":"sir-david-nodenborough"}] |
| references | {"referenceProperty": [{"target":"sir-david-nodenborough"}]} |
Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint"

# checks for referenceName
Scenario: Try to reference nodes in an undefined property:
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "i-do-not-exist" |
| references | [{"target":"anthony-destinode"}] |
| references | {"i-do-not-exist": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1618670106

Scenario: Try to reference nodes in a property that is not of type reference(s):
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "nonReferenceProperty" |
| references | [{"target":"anthony-destinode"}] |
| references | {"nonReferenceProperty": [{"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1618670106

Scenario: Try to reference a node aggregate using a property the reference does not declare
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| nodeAggregateId | "nody-mc-nodeface" |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referencePropertyWithProperties" |
| references | [{"target":"anthony-destinode", "properties":{"i-do-not-exist": "whatever"}}] |
| references | {"referencePropertyWithProperties": [{"target":"anthony-destinode", "properties":{"i-do-not-exist": "whatever"}}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1658406662

Scenario: Try to set a property with a value of a wrong type
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| nodeAggregateId | "nody-mc-nodeface" |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referencePropertyWithProperties" |
| references | [{"target":"anthony-destinode", "properties":{"postalAddress": "28 31st of February Street"}}] |
| references | {"referencePropertyWithProperties": [{"target":"anthony-destinode", "properties":{"postalAddress": "28 31st of February Street"}}]} |
Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1658406762

Scenario: Node reference cannot hold multiple targets to the same node
When the command SetNodeReferences is executed with payload and exceptions are caught:
| Key | Value |
| sourceNodeAggregateId | "source-nodandaise" |
| referenceName | "referencesProperty" |
| references | [{"target":"anthony-destinode"}, {"target":"anthony-destinode"}] |
| references | {"referencesProperty": [{"target":"anthony-destinode"}, {"target":"anthony-destinode"}]} |
Then the last command should have thrown an exception of type "InvalidArgumentException" with code 1700150910
Loading

0 comments on commit 1612659

Please sign in to comment.