diff --git a/.composer.json b/.composer.json
index 67054c5642d..84335a184d5 100644
--- a/.composer.json
+++ b/.composer.json
@@ -4,6 +4,7 @@
"license": ["GPL-3.0-or-later"],
"type": "neos-package-collection",
"require": {
+ "neos/flow-development-collection": "9.0.x-dev"
},
"replace": {
},
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 901b635eea4..e31c154b3d9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2']
+ php-versions: ['8.2', '8.3']
dependencies: ['highest']
composer-arguments: [''] # to run --ignore-platform-reqs in experimental builds
diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php
index 88477f51f32..7a724ed2846 100644
--- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php
+++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php
@@ -122,7 +122,7 @@ private function setupTables(): SetupResult
throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914);
}
- $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNamePrefix))->buildSchema();
+ $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNamePrefix))->buildSchema($schemaManager);
$schemaDiff = (new Comparator())->compare($schemaManager->createSchema(), $schema);
foreach ($schemaDiff->toSaveSql($connection->getDatabasePlatform()) as $statement) {
@@ -807,7 +807,7 @@ function (NodeRecord $node) use ($eventEnvelope) {
'nodeanchorpoint' => $nodeAnchorPoint?->value,
'destinationnodeaggregateid' => $reference->targetNodeAggregateId->value,
'properties' => $reference->properties
- ? \json_encode($reference->properties, JSON_THROW_ON_ERROR)
+ ? \json_encode($reference->properties, JSON_THROW_ON_ERROR & JSON_FORCE_OBJECT)
: null
]);
$position++;
diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php
index 7a21d326589..93bbb781c1b 100644
--- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php
+++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php
@@ -2,144 +2,107 @@
namespace Neos\ContentGraph\DoctrineDbalAdapter;
+use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
+use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory;
/**
* @internal
*/
class DoctrineDbalContentGraphSchemaBuilder
{
+ private const DEFAULT_TEXT_COLLATION = 'utf8mb4_unicode_520_ci';
+
public function __construct(
- private readonly string $tableNamePrefix,
+ private readonly string $tableNamePrefix
) {
}
- public function buildSchema(): Schema
+ public function buildSchema(AbstractSchemaManager $schemaManager): Schema
{
- $schema = new Schema();
-
- $this->createNodeTable($schema);
- $this->createHierarchyRelationTable($schema);
- $this->createReferenceRelationTable($schema);
- $this->createRestrictionRelationTable($schema);
-
- return $schema;
+ return DbalSchemaFactory::createSchemaWithTables($schemaManager, [
+ $this->createNodeTable(),
+ $this->createHierarchyRelationTable(),
+ $this->createReferenceRelationTable(),
+ $this->createRestrictionRelationTable()
+ ]);
}
- private function createNodeTable(Schema $schema): void
+ private function createNodeTable(): Table
{
+ $table = new Table($this->tableNamePrefix . '_node', [
+ DbalSchemaFactory::columnForNodeAnchorPoint('relationanchorpoint'),
+ DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false),
+ DbalSchemaFactory::columnForDimensionSpacePoint('origindimensionspacepoint')->setNotnull(false),
+ DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false),
+ DbalSchemaFactory::columnForNodeTypeName('nodetypename'),
+ (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('classification', Type::getType(Types::STRING)))->setLength(20)->setNotnull(true)->setCustomSchemaOption('charset', 'binary'),
+ (new Column('created', Type::getType(Types::DATETIME_IMMUTABLE)))->setDefault('CURRENT_TIMESTAMP')->setNotnull(true),
+ (new Column('originalcreated', Type::getType(Types::DATETIME_IMMUTABLE)))->setDefault('CURRENT_TIMESTAMP')->setNotnull(true),
+ (new Column('lastmodified', Type::getType(Types::DATETIME_IMMUTABLE)))->setNotnull(false)->setDefault(null),
+ (new Column('originallastmodified', Type::getType(Types::DATETIME_IMMUTABLE)))->setNotnull(false)->setDefault(null)
+ ]);
- $table = $schema->createTable($this->tableNamePrefix . '_node');
- $table->addColumn('relationanchorpoint', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('nodeaggregateid', Types::STRING)
- ->setLength(64)
- ->setNotnull(false);
- $table->addColumn('origindimensionspacepoint', Types::TEXT)
- ->setNotnull(false);
- $table->addColumn('origindimensionspacepointhash', Types::STRING)
- ->setLength(255)
- ->setNotnull(false);
- $table->addColumn('nodetypename', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('properties', Types::TEXT) // TODO longtext?
- ->setNotnull(true);
- $table->addColumn('classification', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('created', Types::DATETIME_IMMUTABLE)
- ->setDefault('CURRENT_TIMESTAMP')
- ->setNotnull(true);
- $table->addColumn('originalcreated', Types::DATETIME_IMMUTABLE)
- ->setDefault('CURRENT_TIMESTAMP')
- ->setNotnull(true);
- $table->addColumn('lastmodified', Types::DATETIME_IMMUTABLE)
- ->setNotnull(false)
- ->setDefault(null);
- $table->addColumn('originallastmodified', Types::DATETIME_IMMUTABLE)
- ->setNotnull(false)
- ->setDefault(null);
- $table
+ return $table
->setPrimaryKey(['relationanchorpoint'])
->addIndex(['nodeaggregateid'])
->addIndex(['nodetypename']);
}
- private function createHierarchyRelationTable(Schema $schema): void
+ private function createHierarchyRelationTable(): Table
{
- $table = $schema->createTable($this->tableNamePrefix . '_hierarchyrelation');
- $table->addColumn('name', Types::STRING)
- ->setLength(255)
- ->setNotnull(false);
- $table->addColumn('position', Types::INTEGER)
- ->setNotnull(true);
- $table->addColumn('contentstreamid', Types::STRING)
- ->setLength(40)
- ->setNotnull(true);
- $table->addColumn('dimensionspacepoint', Types::TEXT)
- ->setNotnull(true);
- $table->addColumn('dimensionspacepointhash', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('parentnodeanchor', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('childnodeanchor', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table
+ $table = new Table($this->tableNamePrefix . '_hierarchyrelation', [
+ (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true),
+ DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true),
+ DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true),
+ DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true),
+ DbalSchemaFactory::columnForNodeAnchorPoint('parentnodeanchor'),
+ DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor')
+ ]);
+
+ return $table
->addIndex(['childnodeanchor'])
->addIndex(['contentstreamid'])
->addIndex(['parentnodeanchor'])
+ ->addIndex(['contentstreamid', 'childnodeanchor', 'dimensionspacepointhash'])
->addIndex(['contentstreamid', 'dimensionspacepointhash']);
}
- private function createReferenceRelationTable(Schema $schema): void
+ private function createReferenceRelationTable(): Table
{
- $table = $schema->createTable($this->tableNamePrefix . '_referencerelation');
- $table->addColumn('name', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('position', Types::INTEGER)
- ->setNotnull(true);
- $table->addColumn('nodeanchorpoint', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('properties', Types::TEXT)
- ->setNotnull(false);
- $table->addColumn('destinationnodeaggregateid', Types::STRING)
- ->setLength(64)
- ->setNotnull(true);
+ $table = new Table($this->tableNamePrefix . '_referencerelation', [
+ (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'),
+ (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true),
+ DbalSchemaFactory::columnForNodeAnchorPoint('nodeanchorpoint'),
+ (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(false)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ DbalSchemaFactory::columnForNodeAggregateId('destinationnodeaggregateid')->setNotnull(true)
+ ]);
- $table
+ return $table
->setPrimaryKey(['name', 'position', 'nodeanchorpoint']);
}
- private function createRestrictionRelationTable(Schema $schema): void
+ private function createRestrictionRelationTable(): Table
{
- $table = $schema->createTable($this->tableNamePrefix . '_restrictionrelation');
- $table->addColumn('contentstreamid', Types::STRING)
- ->setLength(40)
- ->setNotnull(true);
- $table->addColumn('dimensionspacepointhash', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $table->addColumn('originnodeaggregateid', Types::STRING)
- ->setLength(64)
- ->setNotnull(true);
- $table->addColumn('affectednodeaggregateid', Types::STRING)
- ->setLength(64)
- ->setNotnull(true);
+ $table = new Table($this->tableNamePrefix . '_restrictionrelation', [
+ DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true),
+ DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true),
+ DbalSchemaFactory::columnForNodeAggregateId('originnodeaggregateid')->setNotnull(false),
+ DbalSchemaFactory::columnForNodeAggregateId('affectednodeaggregateid')->setNotnull(false),
+ ]);
- $table
- ->setPrimaryKey([
- 'contentstreamid',
- 'dimensionspacepointhash',
- 'originnodeaggregateid',
- 'affectednodeaggregateid'
- ]);
+ return $table->setPrimaryKey([
+ 'contentstreamid',
+ 'dimensionspacepointhash',
+ 'originnodeaggregateid',
+ 'affectednodeaggregateid'
+ ]);
}
}
diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php
index 3e39613f33b..3ede770bd5d 100644
--- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php
+++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php
@@ -15,36 +15,48 @@
namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository;
use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Driver\Exception as DriverException;
+use Doctrine\DBAL\Exception as DBALException;
+use Doctrine\DBAL\Query\QueryBuilder;
+use Doctrine\DBAL\Result;
use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection;
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint;
+use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet;
+use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
-use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches;
-use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter;
-use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregates;
-use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist;
-use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException;
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
-use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
-use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification;
-use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
-use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
-use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
+use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface;
+use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
+use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate;
-use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
-use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
+use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregates;
+use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
+use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist;
+use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
-use Neos\ContentRepository\Core\NodeType\NodeTypeName;
+use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
+use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
/**
* The Doctrine DBAL adapter content graph
*
* To be used as a read-only source of nodes
*
+ * ## Conventions for SQL queries
+ *
+ * - n -> node
+ * - h -> hierarchy edge
+ *
+ * - if more than one node (parent-child)
+ * - pn -> parent node
+ * - cn -> child node
+ * - h -> the hierarchy edge connecting parent and child
+ * - ph -> the hierarchy edge incoming to the parent (sometimes relevant)
+ *
* @internal the parent interface {@see ContentGraphInterface} is API
*/
final class ContentGraph implements ContentGraphInterface
@@ -124,289 +136,159 @@ public function findRootNodeAggregates(
ContentStreamId $contentStreamId,
FindRootNodeAggregatesFilter $filter,
): NodeAggregates {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint
- FROM ' . $this->tableNamePrefix . '_node n
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h
- ON h.childnodeanchor = n.relationanchorpoint
- WHERE h.contentstreamid = :contentStreamId
- AND h.parentnodeanchor = :rootEdgeParentAnchorId ';
-
- $parameters = [
- 'contentStreamId' => $contentStreamId->value,
- 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value,
- ];
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint')
+ ->from($this->tableNamePrefix . '_node', 'n')
+ ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint')
+ ->where('h.contentstreamid = :contentStreamId')
+ ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId')
+ ->setParameters([
+ 'contentStreamId' => $contentStreamId->value,
+ 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value,
+ ]);
if ($filter->nodeTypeName !== null) {
- $query .= ' AND n.nodetypename = :nodeTypeName';
- $parameters['nodeTypeName'] = $filter->nodeTypeName->value;
+ $queryBuilder
+ ->andWhere('n.nodetypename = :nodeTypeName')
+ ->setParameter('nodeTypeName', $filter->nodeTypeName->value);
}
-
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
-
- /** @var \Traversable $nodeAggregates The factory will return a NodeAggregate since the array is not empty */
- $nodeAggregates = $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $nodeRows,
- VisibilityConstraints::withoutRestrictions()
- );
-
- return NodeAggregates::fromArray(iterator_to_array($nodeAggregates));
+ return NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($queryBuilder)));
}
public function findNodeAggregatesByType(
ContentStreamId $contentStreamId,
NodeTypeName $nodeTypeName
): iterable {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint
- FROM ' . $this->tableNamePrefix . '_node n
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h
- ON h.childnodeanchor = n.relationanchorpoint
- WHERE h.contentstreamid = :contentStreamId
- AND n.nodetypename = :nodeTypeName';
-
- $parameters = [
- 'contentStreamId' => $contentStreamId->value,
- 'nodeTypeName' => $nodeTypeName->value,
- ];
-
- $resultStatement = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
- return $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $resultStatement,
- VisibilityConstraints::withoutRestrictions()
- );
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint')
+ ->from($this->tableNamePrefix . '_node', 'n')
+ ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint')
+ ->where('h.contentstreamid = :contentStreamId')
+ ->andWhere('n.nodetypename = :nodeTypeName')
+ ->setParameters([
+ 'contentStreamId' => $contentStreamId->value,
+ 'nodeTypeName' => $nodeTypeName->value,
+ ]);
+ return $this->mapQueryBuilderToNodeAggregates($queryBuilder);
}
- /**
- * @throws DBALException
- * @throws \Exception
- */
public function findNodeAggregateById(
ContentStreamId $contentStreamId,
NodeAggregateId $nodeAggregateId
): ?NodeAggregate {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT n.*,
- h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint,
- r.dimensionspacepointhash AS disableddimensionspacepointhash
- FROM ' . $this->tableNamePrefix . '_hierarchyrelation h
- JOIN ' . $this->tableNamePrefix . '_node n ON n.relationanchorpoint = h.childnodeanchor
- LEFT JOIN ' . $this->tableNamePrefix . '_restrictionrelation r
- ON r.originnodeaggregateid = n.nodeaggregateid
- AND r.contentstreamid = h.contentstreamid
- AND r.affectednodeaggregateid = n.nodeaggregateid
- AND r.dimensionspacepointhash = h.dimensionspacepointhash
- WHERE n.nodeaggregateid = :nodeAggregateId
- AND h.contentstreamid = :contentStreamId';
- $parameters = [
- 'nodeAggregateId' => $nodeAggregateId->value,
- 'contentStreamId' => $contentStreamId->value
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('n.*, h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash')
+ ->from($this->tableNamePrefix . '_hierarchyrelation', 'h')
+ ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor')
+ ->leftJoin('h', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = n.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = n.nodeaggregateid AND r.dimensionspacepointhash = h.dimensionspacepointhash')
+ ->where('n.nodeaggregateid = :nodeAggregateId')
+ ->andWhere('h.contentstreamid = :contentStreamId')
+ ->setParameters([
+ 'nodeAggregateId' => $nodeAggregateId->value,
+ 'contentStreamId' => $contentStreamId->value
+ ]);
return $this->nodeFactory->mapNodeRowsToNodeAggregate(
- $nodeRows,
+ $this->fetchRows($queryBuilder),
VisibilityConstraints::withoutRestrictions()
);
}
/**
* @return iterable
- * @throws DBALException
- * @throws \Exception
*/
public function findParentNodeAggregates(
ContentStreamId $contentStreamId,
NodeAggregateId $childNodeAggregateId
): iterable {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT p.*,
- ph.name, ph.contentstreamid, ph.dimensionspacepoint AS covereddimensionspacepoint,
- r.dimensionspacepointhash AS disableddimensionspacepointhash
- FROM ' . $this->tableNamePrefix . '_node p
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph
- ON ph.childnodeanchor = p.relationanchorpoint
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch
- ON ch.parentnodeanchor = p.relationanchorpoint
- JOIN ' . $this->tableNamePrefix . '_node c ON ch.childnodeanchor = c.relationanchorpoint
- LEFT JOIN ' . $this->tableNamePrefix . '_restrictionrelation r
- ON r.originnodeaggregateid = p.nodeaggregateid
- AND r.contentstreamid = ph.contentstreamid
- AND r.affectednodeaggregateid = p.nodeaggregateid
- AND r.dimensionspacepointhash = ph.dimensionspacepointhash
- WHERE c.nodeaggregateid = :nodeAggregateId
- AND ph.contentstreamid = :contentStreamId
- AND ch.contentstreamid = :contentStreamId';
- $parameters = [
- 'nodeAggregateId' => $childNodeAggregateId->value,
- 'contentStreamId' => $contentStreamId->value
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
- return $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $nodeRows,
- VisibilityConstraints::withoutRestrictions()
- );
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('pn.*, ph.name, ph.contentstreamid, ph.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash')
+ ->from($this->tableNamePrefix . '_node', 'pn')
+ ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint')
+ ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint')
+ ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor')
+ ->leftJoin('ph', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = pn.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = pn.nodeaggregateid AND r.dimensionspacepointhash = ph.dimensionspacepointhash')
+ ->where('cn.nodeaggregateid = :nodeAggregateId')
+ ->andWhere('ph.contentstreamid = :contentStreamId')
+ ->andWhere('ch.contentstreamid = :contentStreamId')
+ ->setParameters([
+ 'nodeAggregateId' => $childNodeAggregateId->value,
+ 'contentStreamId' => $contentStreamId->value
+ ]);
+
+ return $this->mapQueryBuilderToNodeAggregates($queryBuilder);
}
- /**
- * @throws DBALException
- * @throws \Exception
- */
public function findParentNodeAggregateByChildOriginDimensionSpacePoint(
ContentStreamId $contentStreamId,
NodeAggregateId $childNodeAggregateId,
OriginDimensionSpacePoint $childOriginDimensionSpacePoint
): ?NodeAggregate {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT n.*,
- h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint,
- r.dimensionspacepointhash AS disableddimensionspacepointhash
- FROM ' . $this->tableNamePrefix . '_node n
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h
- ON h.childnodeanchor = n.relationanchorpoint
- LEFT JOIN ' . $this->tableNamePrefix . '_restrictionrelation r
- ON r.originnodeaggregateid = n.nodeaggregateid
- AND r.contentstreamid = h.contentstreamid
- AND r.affectednodeaggregateid = n.nodeaggregateid
- AND r.dimensionspacepointhash = h.dimensionspacepointhash
- WHERE n.nodeaggregateid = (
- SELECT p.nodeaggregateid FROM ' . $this->tableNamePrefix . '_node p
- INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch
- ON ch.parentnodeanchor = p.relationanchorpoint
- INNER JOIN ' . $this->tableNamePrefix . '_node c
- ON ch.childnodeanchor = c.relationanchorpoint
- WHERE ch.contentstreamid = :contentStreamId
- AND ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash
- AND c.nodeaggregateid = :childNodeAggregateId
- AND c.origindimensionspacepointhash = :childOriginDimensionSpacePointHash
- )
- AND h.contentstreamid = :contentStreamId';
-
- $parameters = [
- 'contentStreamId' => $contentStreamId->value,
- 'childNodeAggregateId' => $childNodeAggregateId->value,
- 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash,
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
+ $subQueryBuilder = $this->createQueryBuilder()
+ ->select('pn.nodeaggregateid')
+ ->from($this->tableNamePrefix . '_node', 'pn')
+ ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint')
+ ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor')
+ ->where('ch.contentstreamid = :contentStreamId')
+ ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash')
+ ->andWhere('cn.nodeaggregateid = :childNodeAggregateId')
+ ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash');
+
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('n.*, h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash')
+ ->from($this->tableNamePrefix . '_node', 'n')
+ ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint')
+ ->leftJoin('h', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = n.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = n.nodeaggregateid AND r.dimensionspacepointhash = h.dimensionspacepointhash')
+ ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')')
+ ->andWhere('h.contentstreamid = :contentStreamId')
+ ->setParameters([
+ 'contentStreamId' => $contentStreamId->value,
+ 'childNodeAggregateId' => $childNodeAggregateId->value,
+ 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash,
+ ]);
return $this->nodeFactory->mapNodeRowsToNodeAggregate(
- $nodeRows,
+ $this->fetchRows($queryBuilder),
VisibilityConstraints::withoutRestrictions()
);
}
/**
* @return iterable
- * @throws DBALException|\Exception
*/
public function findChildNodeAggregates(
ContentStreamId $contentStreamId,
NodeAggregateId $parentNodeAggregateId
): iterable {
- $connection = $this->client->getConnection();
-
- $query = $this->createChildNodeAggregateQuery();
-
- $parameters = [
- 'parentNodeAggregateId' => $parentNodeAggregateId->value,
- 'contentStreamId' => $contentStreamId->value
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
- return $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $nodeRows,
- VisibilityConstraints::withoutRestrictions()
- );
+ $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId);
+ return $this->mapQueryBuilderToNodeAggregates($queryBuilder);
}
/**
* @return iterable
- * @throws DBALException|NodeTypeNotFoundException
*/
public function findChildNodeAggregatesByName(
ContentStreamId $contentStreamId,
NodeAggregateId $parentNodeAggregateId,
NodeName $name
): iterable {
- $connection = $this->client->getConnection();
-
- $query = $this->createChildNodeAggregateQuery() . '
- AND ch.name = :relationName';
-
- $parameters = [
- 'contentStreamId' => $contentStreamId->value,
- 'parentNodeAggregateId' => $parentNodeAggregateId->value,
- 'relationName' => $name->value
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
- return $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $nodeRows,
- VisibilityConstraints::withoutRestrictions()
- );
+ $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId)
+ ->andWhere('ch.name = :relationName')
+ ->setParameter('relationName', $name->value);
+ return $this->mapQueryBuilderToNodeAggregates($queryBuilder);
}
/**
* @return iterable
- * @throws DBALException|NodeTypeNotFoundException
*/
public function findTetheredChildNodeAggregates(
ContentStreamId $contentStreamId,
NodeAggregateId $parentNodeAggregateId
): iterable {
- $connection = $this->client->getConnection();
-
- $query = $this->createChildNodeAggregateQuery() . '
- AND c.classification = :tetheredClassification';
-
- $parameters = [
- 'contentStreamId' => $contentStreamId->value,
- 'parentNodeAggregateId' => $parentNodeAggregateId->value,
- 'tetheredClassification' => NodeAggregateClassification::CLASSIFICATION_TETHERED->value
- ];
-
- $nodeRows = $connection->executeQuery($query, $parameters)->fetchAllAssociative();
-
- return $this->nodeFactory->mapNodeRowsToNodeAggregates(
- $nodeRows,
- VisibilityConstraints::withoutRestrictions()
- );
- }
-
- private function createChildNodeAggregateQuery(): string
- {
- return 'SELECT c.*,
- ch.name, ch.contentstreamid, ch.dimensionspacepoint AS covereddimensionspacepoint,
- r.dimensionspacepointhash AS disableddimensionspacepointhash
- FROM ' . $this->tableNamePrefix . '_node p
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph
- ON ph.childnodeanchor = p.relationanchorpoint
- JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch
- ON ch.parentnodeanchor = p.relationanchorpoint
- JOIN ' . $this->tableNamePrefix . '_node c
- ON ch.childnodeanchor = c.relationanchorpoint
- LEFT JOIN ' . $this->tableNamePrefix . '_restrictionrelation r
- ON r.originnodeaggregateid = p.nodeaggregateid
- AND r.contentstreamid = ph.contentstreamid
- AND r.affectednodeaggregateid = p.nodeaggregateid
- AND r.dimensionspacepointhash = ph.dimensionspacepointhash
- WHERE p.nodeaggregateid = :parentNodeAggregateId
- AND ph.contentstreamid = :contentStreamId
- AND ch.contentstreamid = :contentStreamId';
+ $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId)
+ ->andWhere('cn.classification = :tetheredClassification')
+ ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value);
+ return $this->mapQueryBuilderToNodeAggregates($queryBuilder);
}
/**
@@ -416,7 +298,6 @@ private function createChildNodeAggregateQuery(): string
* @param OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint
* @param DimensionSpacePointSet $dimensionSpacePointsToCheck
* @return DimensionSpacePointSet
- * @throws DBALException
*/
public function getDimensionSpacePointsOccupiedByChildNodeName(
ContentStreamId $contentStreamId,
@@ -425,62 +306,55 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(
OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint,
DimensionSpacePointSet $dimensionSpacePointsToCheck
): DimensionSpacePointSet {
- $connection = $this->client->getConnection();
-
- $query = 'SELECT h.dimensionspacepoint, h.dimensionspacepointhash
- FROM ' . $this->tableNamePrefix . '_hierarchyrelation h
- INNER JOIN ' . $this->tableNamePrefix . '_node n
- ON h.parentnodeanchor = n.relationanchorpoint
- INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph
- ON ph.childnodeanchor = n.relationanchorpoint
- WHERE n.nodeaggregateid = :parentNodeAggregateId
- AND n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash
- AND ph.contentstreamid = :contentStreamId
- AND h.contentstreamid = :contentStreamId
- AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes)
- AND h.name = :nodeName';
- $parameters = [
- 'parentNodeAggregateId' => $parentNodeAggregateId->value,
- 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash,
- 'contentStreamId' => $contentStreamId->value,
- 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(),
- 'nodeName' => $nodeName->value
- ];
- $types = [
- 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY
- ];
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('h.dimensionspacepoint, h.dimensionspacepointhash')
+ ->from($this->tableNamePrefix . '_hierarchyrelation', 'h')
+ ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.parentnodeanchor')
+ ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = n.relationanchorpoint')
+ ->where('n.nodeaggregateid = :parentNodeAggregateId')
+ ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash')
+ ->andWhere('ph.contentstreamid = :contentStreamId')
+ ->andWhere('h.contentstreamid = :contentStreamId')
+ ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)')
+ ->andWhere('h.name = :nodeName')
+ ->setParameters([
+ 'parentNodeAggregateId' => $parentNodeAggregateId->value,
+ 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash,
+ 'contentStreamId' => $contentStreamId->value,
+ 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(),
+ 'nodeName' => $nodeName->value
+ ], [
+ 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY,
+ ]);
$dimensionSpacePoints = [];
- foreach (
- $connection->executeQuery($query, $parameters, $types)
- ->fetchAllAssociative() as $hierarchyRelationData
- ) {
- $dimensionSpacePoints[$hierarchyRelationData['dimensionspacepointhash']]
- = DimensionSpacePoint::fromJsonString($hierarchyRelationData['dimensionspacepoint']);
+ foreach ($this->fetchRows($queryBuilder) as $hierarchyRelationData) {
+ $dimensionSpacePoints[$hierarchyRelationData['dimensionspacepointhash']] = DimensionSpacePoint::fromJsonString($hierarchyRelationData['dimensionspacepoint']);
}
-
return new DimensionSpacePointSet($dimensionSpacePoints);
}
public function countNodes(): int
{
- $connection = $this->client->getConnection();
- $query = 'SELECT COUNT(*) FROM ' . $this->tableNamePrefix . '_node';
-
- $row = $connection->executeQuery($query)->fetchAssociative();
-
- return $row ? (int)$row['COUNT(*)'] : 0;
+ $queryBuilder = $this->createQueryBuilder()
+ ->select('COUNT(*)')
+ ->from($this->tableNamePrefix . '_node');
+ $result = $queryBuilder->execute();
+ if (!$result instanceof Result) {
+ throw new \RuntimeException(sprintf('Failed to count nodes. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701444550);
+ }
+ try {
+ return (int)$result->fetchOne();
+ } catch (DriverException | DBALException $e) {
+ throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444590, $e);
+ }
}
public function findUsedNodeTypeNames(): iterable
{
- $connection = $this->client->getConnection();
-
- $rows = $connection->executeQuery('SELECT DISTINCT nodetypename FROM ' . $this->tableNamePrefix . '_node')
- ->fetchAllAssociative();
-
- return array_map(function (array $row) {
- return NodeTypeName::fromString($row['nodetypename']);
- }, $rows);
+ $rows = $this->fetchRows($this->createQueryBuilder()
+ ->select('DISTINCT nodetypename')
+ ->from($this->tableNamePrefix . '_node'));
+ return array_map(static fn (array $row) => NodeTypeName::fromString($row['nodetypename']), $rows);
}
/**
@@ -491,4 +365,56 @@ public function getSubgraphs(): array
{
return $this->subgraphs;
}
+
+ private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder
+ {
+ return $this->createQueryBuilder()
+ ->select('cn.*, ch.name, ch.contentstreamid, ch.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash')
+ ->from($this->tableNamePrefix . '_node', 'pn')
+ ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint')
+ ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint')
+ ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor')
+ ->leftJoin('pn', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = pn.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = pn.nodeaggregateid AND r.dimensionspacepointhash = ph.dimensionspacepointhash')
+ ->where('pn.nodeaggregateid = :parentNodeAggregateId')
+ ->andWhere('ph.contentstreamid = :contentStreamId')
+ ->andWhere('ch.contentstreamid = :contentStreamId')
+ ->orderBy('ch.position')
+ ->setParameters([
+ 'parentNodeAggregateId' => $parentNodeAggregateId->value,
+ 'contentStreamId' => $contentStreamId->value,
+ ]);
+ }
+
+ private function createQueryBuilder(): QueryBuilder
+ {
+ return $this->client->getConnection()->createQueryBuilder();
+ }
+
+ /**
+ * @param QueryBuilder $queryBuilder
+ * @return iterable
+ */
+ private function mapQueryBuilderToNodeAggregates(QueryBuilder $queryBuilder): iterable
+ {
+ return $this->nodeFactory->mapNodeRowsToNodeAggregates(
+ $this->fetchRows($queryBuilder),
+ VisibilityConstraints::withoutRestrictions()
+ );
+ }
+
+ /**
+ * @return array>
+ */
+ private function fetchRows(QueryBuilder $queryBuilder): array
+ {
+ $result = $queryBuilder->execute();
+ if (!$result instanceof Result) {
+ throw new \RuntimeException(sprintf('Failed to execute query. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701443535);
+ }
+ try {
+ return $result->fetchAllAssociative();
+ } catch (DriverException | DBALException $e) {
+ throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444358, $e);
+ }
+ }
}
diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php
index 4100d614945..aa77993c975 100644
--- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php
+++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php
@@ -175,7 +175,7 @@ public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node
->where('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value)
->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value)
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash);
- $this->addRestrictionRelationConstraints($queryBuilder);
+ $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
return $this->fetchNode($queryBuilder);
}
@@ -190,7 +190,7 @@ public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash)
->andWhere('n.classification = :nodeAggregateClassification')
->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value);
- $this->addRestrictionRelationConstraints($queryBuilder);
+ $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
return $this->fetchNode($queryBuilder);
}
@@ -207,12 +207,14 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node
->andWhere('ch.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash)
->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash');
- $this->addRestrictionRelationConstraints($queryBuilder, 'cn', 'ch');
+ $this->addRestrictionRelationConstraints($queryBuilder, 'cn', 'ch', ':contentStreamId', ':dimensionSpacePointHash');
return $this->fetchNode($queryBuilder);
}
- public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node
+ public function findNodeByPath(NodePath|NodeName $path, NodeAggregateId $startingNodeAggregateId): ?Node
{
+ $path = $path instanceof NodeName ? NodePath::fromNodeNames($path) : $path;
+
$startingNode = $this->findNodeById($startingNodeAggregateId);
return $startingNode
@@ -229,7 +231,12 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node
: null;
}
- public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $edgeName): ?Node
+ /**
+ * Find a single child node by its name
+ *
+ * @return Node|null the node that is connected to its parent with the specified $nodeName, or NULL if no matching node exists or the parent node is not accessible
+ */
+ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node
{
$queryBuilder = $this->createQueryBuilder()
->select('cn.*, h.name, h.contentstreamid')
@@ -239,8 +246,8 @@ public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNod
->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value)
->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value)
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash)
- ->andWhere('h.name = :edgeName')->setParameter('edgeName', $edgeName->value);
- $this->addRestrictionRelationConstraints($queryBuilder, 'cn');
+ ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value);
+ $this->addRestrictionRelationConstraints($queryBuilder, 'cn', 'h', ':contentStreamId', ':dimensionSpacePointHash');
return $this->fetchNode($queryBuilder);
}
@@ -289,7 +296,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi
->where('h.contentstreamid = :contentStreamId')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('n.nodeaggregateid = :entryNodeAggregateId');
- $this->addRestrictionRelationConstraints($queryBuilderInitial);
+ $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderRecursive = $this->createQueryBuilder()
->select('c.*, h.name, h.contentstreamid, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position')
@@ -304,7 +311,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi
if ($filter->nodeTypes !== null) {
$this->addNodeTypeCriteria($queryBuilderRecursive, $filter->nodeTypes, 'c');
}
- $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c');
+ $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderCte = $this->createQueryBuilder()
->select('*')
@@ -381,7 +388,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose
->andWhere('ph.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('n.nodeaggregateid = :entryNodeAggregateId');
- $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph');
+ $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderRecursive = $this->createQueryBuilder()
->select('p.*, h.name, h.contentstreamid, h.parentnodeanchor')
@@ -390,7 +397,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose
->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = p.relationanchorpoint')
->where('h.contentstreamid = :contentStreamId')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash');
- $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p');
+ $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderCte = $this->createQueryBuilder()
->select('*')
@@ -472,7 +479,6 @@ private function findNodeByPathFromStartingNode(NodePath $path, Node $startingNo
$currentNode = $startingNode;
foreach ($path->getParts() as $edgeName) {
- // id exists here :)
$currentNode = $this->findChildNodeConnectedThroughEdgeName($currentNode->nodeAggregateId, $edgeName);
if ($currentNode === null) {
return null;
@@ -491,18 +497,32 @@ private function createUniqueParameterName(): string
return 'param_' . (++$this->dynamicParameterCount);
}
- private function addRestrictionRelationConstraints(QueryBuilder $queryBuilder, string $nodeTableAlias = 'n', string $hierarchyRelationTableAlias = 'h'): void
+ /**
+ * @param QueryBuilder $queryBuilder
+ * @param string $nodeTableAlias
+ * @param string $hierarchyRelationTableAlias
+ * @param string|null $contentStreamIdParameter if not given, condition will be on the hierachy relation content stream id
+ * @param string|null $dimensionspacePointHashParameter if not given, condition will be on the hierachy relation dimension space point hash
+ * @return void
+ */
+ private function addRestrictionRelationConstraints(QueryBuilder $queryBuilder, string $nodeTableAlias = 'n', string $hierarchyRelationTableAlias = 'h', ?string $contentStreamIdParameter = null, ?string $dimensionspacePointHashParameter = null): void
{
if ($this->visibilityConstraints->isDisabledContentShown()) {
return;
}
+
$nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.';
$hierarchyRelationTablePrefix = $hierarchyRelationTableAlias === '' ? '' : $hierarchyRelationTableAlias . '.';
+
+ $contentStreamIdCondition = 'r.contentstreamid = ' . ($contentStreamIdParameter ?? ($hierarchyRelationTablePrefix . 'contentstreamid'));
+
+ $dimensionspacePointHashCondition = 'r.dimensionspacepointhash = ' . ($dimensionspacePointHashParameter ?? ($hierarchyRelationTablePrefix . 'dimensionspacepointhash'));
+
$subQueryBuilder = $this->createQueryBuilder()
->select('1')
->from($this->tableNamePrefix . '_restrictionrelation', 'r')
- ->where('r.contentstreamid = ' . $hierarchyRelationTablePrefix . 'contentstreamid')
- ->andWhere('r.dimensionspacepointhash = ' . $hierarchyRelationTablePrefix . 'dimensionspacepointhash')
+ ->where($contentStreamIdCondition)
+ ->andWhere($dimensionspacePointHashCondition)
->andWhere('r.affectednodeaggregateid = ' . $nodeTablePrefix . 'nodeaggregateid');
$queryBuilder->andWhere(
'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
@@ -620,7 +640,7 @@ private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, Fi
if ($filter->propertyValue !== null) {
$this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue);
}
- $this->addRestrictionRelationConstraints($queryBuilder);
+ $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
return $queryBuilder;
}
@@ -640,8 +660,8 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod
->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('dh.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value)
->andWhere('sh.contentstreamid = :contentStreamId');
- $this->addRestrictionRelationConstraints($queryBuilder, 'dn', 'dh');
- $this->addRestrictionRelationConstraints($queryBuilder, 'sn', 'sh');
+ $this->addRestrictionRelationConstraints($queryBuilder, 'dn', 'dh', ':contentStreamId', ':dimensionSpacePointHash');
+ $this->addRestrictionRelationConstraints($queryBuilder, 'sn', 'sh', ':contentStreamId', ':dimensionSpacePointHash');
if ($filter->nodeTypes !== null) {
$this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes, "{$destinationTablePrefix}n");
}
@@ -707,7 +727,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod
->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')')
->orderBy('h.position', $preceding ? 'DESC' : 'ASC');
- $this->addRestrictionRelationConstraints($queryBuilder);
+ $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
if ($filter->nodeTypes !== null) {
$this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes);
}
@@ -740,8 +760,8 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId
->andWhere('ph.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('c.nodeaggregateid = :entryNodeAggregateId');
- $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph');
- $this->addRestrictionRelationConstraints($queryBuilderInitial, 'c', 'ch');
+ $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph', ':contentStreamId', ':dimensionSpacePointHash');
+ $this->addRestrictionRelationConstraints($queryBuilderInitial, 'c', 'ch', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderRecursive = $this->createQueryBuilder()
->select('p.*, h.name, h.contentstreamid, h.parentnodeanchor')
@@ -750,7 +770,7 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId
->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = p.relationanchorpoint')
->where('h.contentstreamid = :contentStreamId')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash');
- $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p');
+ $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderCte = $this->createQueryBuilder()
->select('*')
@@ -782,7 +802,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate
->andWhere('ph.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('p.nodeaggregateid = :entryNodeAggregateId');
- $this->addRestrictionRelationConstraints($queryBuilderInitial);
+ $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderRecursive = $this->createQueryBuilder()
->select('c.*, h.name, h.contentstreamid, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position')
@@ -791,7 +811,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate
->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor')
->where('h.contentstreamid = :contentStreamId')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash');
- $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c');
+ $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c', 'h', ':contentStreamId', ':dimensionSpacePointHash');
$queryBuilderCte = $this->createQueryBuilder()
->select('*')
diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php
index 32705839a97..cf3aafd476d 100644
--- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php
+++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php
@@ -269,8 +269,10 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node
) : null;
}
- public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node
+ public function findNodeByPath(NodePath|NodeName $path, NodeAggregateId $startingNodeAggregateId): ?Node
{
+ $path = $path instanceof NodeName ? NodePath::fromNodeNames($path) : $path;
+
$startingNode = $this->findNodeById($startingNodeAggregateId);
return $startingNode
@@ -287,9 +289,9 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node
: null;
}
- public function findChildNodeConnectedThroughEdgeName(
+ private function findChildNodeConnectedThroughEdgeName(
NodeAggregateId $parentNodeAggregateId,
- NodeName $edgeName
+ NodeName $nodeName
): ?Node {
$query = HypergraphChildQuery::create(
$this->contentStreamId,
@@ -298,7 +300,7 @@ public function findChildNodeConnectedThroughEdgeName(
);
$query = $query->withDimensionSpacePoint($this->dimensionSpacePoint)
->withRestriction($this->visibilityConstraints)
- ->withChildNodeName($edgeName);
+ ->withChildNodeName($nodeName);
$nodeRow = $query->execute($this->getDatabaseConnection())->fetchAssociative();
diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php
index bfcb0806fc9..24d758de0f4 100644
--- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php
+++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php
@@ -36,7 +36,7 @@ trait CRBehavioralTestsSubjectProvider
* A runtime cache of all content repositories already set up, represented by their ID
* @var array
*/
- protected array $alreadySetUpContentRepositories = [];
+ protected static array $alreadySetUpContentRepositories = [];
protected ?ContentRepository $currentContentRepository = null;
@@ -169,8 +169,9 @@ protected function setUpContentRepository(ContentRepositoryId $contentRepository
* Catch Up process and the testcase reset.
*/
$contentRepository = $this->createContentRepository($contentRepositoryId);
- if (!in_array($contentRepository->id, $this->alreadySetUpContentRepositories)) {
+ if (!in_array($contentRepository->id, self::$alreadySetUpContentRepositories)) {
$contentRepository->setUp();
+ self::$alreadySetUpContentRepositories[] = $contentRepository->id;
}
/** @var EventStoreInterface $eventStore */
$eventStore = (new \ReflectionClass($contentRepository))->getProperty('eventStore')->getValue($contentRepository);
diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindChildNodeConnectedThroughEdgeName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature
similarity index 79%
rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindChildNodeConnectedThroughEdgeName.feature
rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature
index 67898ef2130..9125fd17850 100644
--- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindChildNodeConnectedThroughEdgeName.feature
+++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature
@@ -1,5 +1,5 @@
@contentrepository @adapters=DoctrineDBAL,Postgres
-Feature: Find nodes using the findChildNodeConnectedThroughEdgeName query
+Feature: Find nodes using the findNodeByPath query with node name as path argument
Background:
Given using the following content dimensions:
@@ -88,15 +88,15 @@ Feature: Find nodes using the findChildNodeConnectedThroughEdgeName query
And the graph projection is fully up to date
Scenario:
- # findChildNodeConnectedThroughEdgeName queries without results
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "non-existing" and edge name "non-existing" I expect no node to be returned
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "home" and edge name "non-existing" I expect no node to be returned
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "non-existing" and edge name "home" I expect no node to be returned
+ # findNodeByPath queries without results
+ When I execute the findNodeByPath query for parent node aggregate id "non-existing" and node name "non-existing" as path I expect no node to be returned
+ When I execute the findNodeByPath query for parent node aggregate id "home" and node name "non-existing" as path I expect no node to be returned
+ When I execute the findNodeByPath query for parent node aggregate id "non-existing" and node name "home" as path I expect no node to be returned
# node "a2a2" is disabled and should not be returned
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "a2a" and edge name "a2a2" I expect no node to be returned
+ When I execute the findNodeByPath query for parent node aggregate id "a2a" and node name "a2a2" as path I expect no node to be returned
# node "a2a2" is disabled and should not lead to results if specified as parent node id
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "a2a2" and edge name "a2a2a" I expect no node to be returned
+ When I execute the findNodeByPath query for parent node aggregate id "a2a2" and node name "a2a2a" as path I expect no node to be returned
- # findChildNodeConnectedThroughEdgeName queries with results
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "home" and edge name "contact" I expect the node "contact" to be returned
- When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id "a2a" and edge name "a2a1" I expect the node "a2a1" to be returned
+ # findNodeByPath queries with results
+ When I execute the findNodeByPath query for parent node aggregate id "home" and node name "contact" as path I expect the node "contact" to be returned
+ When I execute the findNodeByPath query for parent node aggregate id "a2a" and node name "a2a1" as path I expect the node "a2a1" to be returned
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php
index 8f67052dd59..fe125c03c8d 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php
@@ -25,10 +25,8 @@
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
/**
- * CreateNodeAggregateWithNode
+ * Creates a new node aggregate with a new node.
*
- * Creates a new node aggregate with a new node in the given `contentStreamId`
- * with the given `nodeAggregateId` and `originDimensionSpacePoint`.
* The node will be appended as child node of the given `parentNodeId` which must cover the given
* `originDimensionSpacePoint`.
*
@@ -97,7 +95,7 @@ public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPrope
* 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()}_
+ * {@see ContentSubgraphInterface::findNodeByPath()}_
*
* The helper method {@see NodeAggregateIdsByNodePaths::createForNodeType()} will generate recursively
* node aggregate ids for every tethered child node:
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php
index bd9edf3a9e1..dadbd5f3e15 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php
@@ -30,7 +30,7 @@
* The properties of {@see CreateNodeAggregateWithNode} are directly serialized; and then this command
* is called and triggers the actual processing.
*
- * @api commands are the write-API of the ContentRepository
+ * @internal implementation detail, use {@see CreateNodeAggregateWithNode} instead.
*/
final class CreateNodeAggregateWithNodeAndSerializedProperties implements
CommandInterface,
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php
index 03cd7bcfe8d..b929ce833e5 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php
@@ -24,11 +24,11 @@
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
/**
- * Set property values for a given node (internal implementation).
+ * Set property values for a given node.
*
* The property values contain the serialized types already, and include type information.
*
- * @api commands are the write-API of the ContentRepository
+ * @internal implementation detail, use {@see SetNodeProperties} instead.
*/
final class SetSerializedNodeProperties implements
CommandInterface,
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php
index 57ba6eaba4a..7e3aa1af39e 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php
@@ -33,7 +33,7 @@ public function __construct(
}
/**
- * @param array $valueAndType
+ * @param array{type:string,value:mixed} $valueAndType
*/
public static function fromArray(array $valueAndType): self
{
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValues.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValues.php
index 9b1f162e6fe..936e941d925 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValues.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValues.php
@@ -43,7 +43,7 @@ public static function createEmpty(): self
}
/**
- * @param array $propertyValues
+ * @param array $propertyValues
*/
public static function fromArray(array $propertyValues): self
{
@@ -129,6 +129,9 @@ public function splitByScope(NodeType $nodeType): array
);
}
+ /**
+ * @phpstan-assert-if-true !null $this->getProperty()
+ */
public function propertyExists(string $propertyName): bool
{
return isset($this->values[$propertyName]);
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php
index 9404160a980..355281c63cb 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php
@@ -25,11 +25,11 @@
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
/**
- * Set property values for a given node (internal implementation).
+ * Set property values for a given node.
*
* The property values contain the serialized types already, and include type information.
*
- * @api commands are the write-API of the ContentRepository
+ * @internal implementation detail, use {@see SetNodeReferences} instead.
*/
final class SetSerializedNodeReferences implements
CommandInterface,
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReference.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReference.php
index 8928a428377..6c090ad60ea 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReference.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReference.php
@@ -20,7 +20,7 @@
/**
* "Raw" / Serialized node reference as saved in the event log // in projections.
*
- * @api used as part of commands/events
+ * @internal
*/
final class SerializedNodeReference implements \JsonSerializable
{
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php
index 85dc99aa39c..ecf9bf32f9e 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php
@@ -21,7 +21,7 @@
* A collection of SerializedNodeReference objects, to be used when creating reference relations.
*
* @implements \IteratorAggregate
- * @api used as part of commands/events
+ * @internal
*/
final readonly class SerializedNodeReferences implements \IteratorAggregate, \Countable, \JsonSerializable
{
diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php
index 0f68cba7042..09f97b8a12c 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php
@@ -197,9 +197,9 @@ private function handleChangeNodeAggregateType(
$node->originDimensionSpacePoint->toDimensionSpacePoint(),
VisibilityConstraints::withoutRestrictions()
);
- $tetheredNode = $subgraph->findChildNodeConnectedThroughEdgeName(
- $node->nodeAggregateId,
- $tetheredNodeName
+ $tetheredNode = $subgraph->findNodeByPath(
+ $tetheredNodeName,
+ $node->nodeAggregateId
);
if ($tetheredNode === null) {
$tetheredNodeAggregateId = $command->tetheredDescendantNodeAggregateIds
diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php
index 0e2ceab99de..ec9b7f83096 100644
--- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php
+++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php
@@ -70,7 +70,7 @@ public static function create(ContentStreamId $contentStreamId, NodeAggregateId
* 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()}_
+ * {@see ContentSubgraphInterface::findNodeByPath()}_
*
* The helper method {@see NodeAggregateIdsByNodePaths::createForNodeType()} will generate recursively
* node aggregate ids for every tethered child node:
diff --git a/Neos.ContentRepository.Core/Classes/Infrastructure/DbalSchemaFactory.php b/Neos.ContentRepository.Core/Classes/Infrastructure/DbalSchemaFactory.php
new file mode 100644
index 00000000000..458b4eeada1
--- /dev/null
+++ b/Neos.ContentRepository.Core/Classes/Infrastructure/DbalSchemaFactory.php
@@ -0,0 +1,129 @@
+setLength(64)
+ ->setCustomSchemaOption('charset', 'ascii')
+ ->setCustomSchemaOption('collation', 'ascii_general_ci');
+ }
+
+ /**
+ * The ContentStreamId is generally a UUID, therefore not a real "string" but at the moment a strified identifier,
+ * we can safely store it as binary string as the charset doesn't matter
+ * for the characters we use (they are all asscii).
+ *
+ * We should however reduce the allowed size to 36 like suggested
+ * here in the schema and as further improvement store the UUID in a more DB friendly format.
+ *
+ * @see ContentStreamId
+ */
+ public static function columnForContentStreamId(string $columnName): Column
+ {
+ return (new Column($columnName, Type::getType(Types::STRING)))
+ ->setLength(36)
+ ->setCustomSchemaOption('charset', 'binary');
+ }
+
+ /**
+ * An anchorpoint can be used in a given projection to link two nodes, it is a purely internal identifier and should
+ * be as performant as possible in queries, the current code uses UUIDs,
+ * so we will store the stringified UUID as binary for now.
+ *
+ * A simpler and faster format would be preferable though, int or a shorter binary format if possible. Fortunately
+ * this is a pure projection information and therefore could change by replay.
+ */
+ public static function columnForNodeAnchorPoint(string $columnName): Column
+ {
+ return (new Column($columnName, Type::getType(Types::BINARY)))
+ ->setLength(36)
+ ->setNotnull(true);
+ }
+
+ /**
+ * DimensionSpacePoints are PHP objects that need to be serialized as JSON, therefore we store them as TEXT for now,
+ * with a sensible collation for case insensitive text search.
+ *
+ * Using a dedicated JSON column format should be considered for the future.
+ *
+ * @see DimensionSpacePoint
+ */
+ public static function columnForDimensionSpacePoint(string $columnName): Column
+ {
+ return (new Column($columnName, Type::getType(Types::TEXT)))
+ ->setDefault('{}')
+ ->setCustomSchemaOption('collation', 'utf8mb4_unicode_520_ci');
+ }
+
+ /**
+ * The hash for a given dimension space point for better query performance. As this is a hash, the size and type of
+ * content is deterministic, a binary type can be used as the actual content is not so important.
+ *
+ * We could imrpove by actually storing the hash in binary form and shortening and fixing the length.
+ *
+ * @see DimensionSpacePoint
+ */
+ public static function columnForDimensionSpacePointHash(string $columnName): Column
+ {
+ return (new Column($columnName, Type::getType(Types::BINARY)))
+ ->setLength(32)
+ ->setDefault('');
+ }
+
+ /**
+ * The NodeTypeName is an ascii string, we should be able to sort it properly, but we don't need unicode here.
+ *
+ * @see NodeTypeName
+ */
+ public static function columnForNodeTypeName(string $columnName): Column
+ {
+ return (new Column($columnName, Type::getType(Types::STRING)))
+ ->setLength(255)
+ ->setNotnull(true)
+ ->setCustomSchemaOption('charset', 'ascii')
+ ->setCustomSchemaOption('collation', 'ascii_general_ci');
+ }
+
+ /**
+ * @param AbstractSchemaManager $schemaManager
+ * @param Table[] $tables
+ * @return Schema
+ */
+ public static function createSchemaWithTables(AbstractSchemaManager $schemaManager, array $tables): Schema
+ {
+ $schemaConfig = $schemaManager->createSchemaConfig();
+ $schemaConfig->setDefaultTableOptions([
+ 'charset' => 'utf8mb4'
+ ]);
+
+ return new Schema($tables, [], $schemaConfig);
+ }
+}
diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php
index f1e861f3beb..6d6369eb3e8 100644
--- a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php
+++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php
@@ -420,7 +420,7 @@ public function getPropertyType(string $propertyName): string
if (!$this->hasProperty($propertyName)) {
throw new \InvalidArgumentException(
- sprintf('NodeType schema has no property "%s" configured. Cannot read its type.', $propertyName),
+ sprintf('NodeType schema has no property "%s" configured for the NodeType "%s". Cannot read its type.', $propertyName, $this->name->value),
1695062252040
);
}
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php
index d6ea986467b..1d36fa18c2d 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php
@@ -24,10 +24,19 @@
* Example:
* root path: '/' results in
* ~ {"rootNodeTypeName": "Neos.ContentRepository:Root", "path": []}
- * non-root path: '//my/site' results in
- * ~ {"rootNodeTypeName": "Neos.ContentRepository:Root", "path": ["my","site"]}
+ * non-root path: '//my-site/main' results in
+ * ~ {"rootNodeTypeName": "Neos.ContentRepository:Root", "path": ["my-site", "main"]}
*
* It describes the hierarchy path of a node to and including its root node in a subgraph.
+ *
+ * To fetch a node via an absolute path use the subgraph: {@see ContentSubgraphInterface::findNodeByAbsolutePath()}
+ *
+ * ```php
+ * $subgraph->findNodeByAbsolutePath(
+ * AbsoluteNodePath::fromString("//my-site/main")
+ * )
+ * ```
+ *
* @api
*/
final class AbsoluteNodePath implements \JsonSerializable
@@ -49,8 +58,14 @@ public static function fromRootNodeTypeNameAndRelativePath(
}
/**
- * The ancestors must be ordered with the root node first, so if you call this using
- * {@see ContentSubgraphInterface::findAncestorNodes()}, you need to call ${@see Nodes::reverse()} first
+ * The ancestors must be ordered with the root node first.
+ *
+ * If you want to retrieve the path of a node using {@see ContentSubgraphInterface::findAncestorNodes()}, you need to reverse the order first {@see Nodes::reverse()}
+ *
+ * ```php
+ * $ancestors = $this->findAncestorNodes($leafNode->nodeAggregateId, FindAncestorNodesFilter::create())->reverse();
+ * $absoluteNodePath = AbsoluteNodePath::fromLeafNodeAndAncestors($leafNode, $ancestors);
+ * ```
*/
public static function fromLeafNodeAndAncestors(Node $leafNode, Nodes $ancestors): self
{
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php
index 7660d7c154d..496e55db2b6 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php
@@ -163,33 +163,18 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node
return $parentNode;
}
- public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node
+ public function findNodeByPath(NodePath|NodeName $path, NodeAggregateId $startingNodeAggregateId): ?Node
{
- // TODO implement runtime caches
+ // TODO: implement runtime caches
return $this->wrappedContentSubgraph->findNodeByPath($path, $startingNodeAggregateId);
}
public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node
{
- // TODO implement runtime caches
+ // TODO: implement runtime caches
return $this->wrappedContentSubgraph->findNodeByAbsolutePath($path);
}
- public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $edgeName): ?Node
- {
- $namedChildNodeCache = $this->inMemoryCache->getNamedChildNodeByNodeIdCache();
- if ($namedChildNodeCache->contains($parentNodeAggregateId, $edgeName)) {
- return $namedChildNodeCache->get($parentNodeAggregateId, $edgeName);
- }
- $node = $this->wrappedContentSubgraph->findChildNodeConnectedThroughEdgeName($parentNodeAggregateId, $edgeName);
- if ($node === null) {
- return null;
- }
- $namedChildNodeCache->add($parentNodeAggregateId, $edgeName, $node);
- $this->inMemoryCache->getNodeByNodeAggregateIdCache()->add($node->nodeAggregateId, $node);
- return $node;
- }
-
public function findSucceedingSiblingNodes(NodeAggregateId $siblingNodeAggregateId, FindSucceedingSiblingNodesFilter $filter): Nodes
{
// TODO implement runtime caches
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php
index 5f65fb6a0f1..e3f5fe29b85 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php
@@ -100,13 +100,6 @@ public function findSucceedingSiblingNodes(NodeAggregateId $siblingNodeAggregate
*/
public function findPrecedingSiblingNodes(NodeAggregateId $siblingNodeAggregateId, Filter\FindPrecedingSiblingNodesFilter $filter): Nodes;
- /**
- * Find a single child node by its name
- *
- * @return Node|null the node that is connected to its parent with the specified $edgeName, or NULL if no matching node exists or the parent node is not accessible
- */
- public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $edgeName): ?Node;
-
/**
* Recursively find all nodes above the $entryNodeAggregateId that match the specified $filter and return them as a flat list
*/
@@ -181,10 +174,12 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, Filter\Cou
/**
* Find a single node underneath $startingNodeAggregateId that matches the specified $path
*
+ * If a node name as $path is given it will be treated as path with a single segment.
+ *
* NOTE: This operation is most likely to be deprecated since the concept of node paths is not really used in the core, and it has some logical issues
* @return Node|null the node that matches the given $path, or NULL if no node on that path is accessible
*/
- public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node;
+ public function findNodeByPath(NodePath|NodeName $path, NodeAggregateId $startingNodeAggregateId): ?Node;
/**
* Find a single node underneath that matches the specified absolute $path
@@ -197,7 +192,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node;
/**
* Determine the absolute path of a node
*
- * @deprecated use ${@see self::findAncestorNodes()} instead
+ * @deprecated use {@see self::findAncestorNodes()} in combination with {@see AbsoluteNodePath::fromLeafNodeAndAncestors()} instead
* @throws \InvalidArgumentException if the node path could not be retrieved because it is inaccessible or contains no valid path. The latter can happen if any node in the hierarchy has no name
*/
public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNodePath;
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php
index d29f4191ec9..55f6e39a563 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php
@@ -29,22 +29,24 @@
*
* The node does not have structure information, i.e. no infos
* about its children. To f.e. fetch children, you need to fetch
- * the subgraph via $node->subgraphIdentity and then
- * call findChildNodes() on the subgraph.
+ * the subgraph {@see ContentGraphInterface::getSubgraph()} via
+ * $subgraphIdentity {@see Node::$subgraphIdentity}. and then
+ * call findChildNodes() {@see ContentSubgraphInterface::findChildNodes()}
+ * on the subgraph.
*
* @api Note: The constructor is not part of the public API
*/
final readonly class Node
{
/**
- * @param ContentSubgraphIdentity $subgraphIdentity This is part of the node's "Read Model" identity which is defined by: {@see self::subgraphIdentity} and {@see self::nodeAggregateId}
+ * @param ContentSubgraphIdentity $subgraphIdentity This is part of the node's "Read Model" identity which is defined by: {@see self::subgraphIdentity} and {@see self::nodeAggregateId}. With this information, you can fetch a Subgraph using {@see ContentGraphInterface::getSubgraph()}.
* @param NodeAggregateId $nodeAggregateId NodeAggregateId (identifier) of this node. This is part of the node's "Read Model" identity which is defined by: {@see self::subgraphIdentity} and {@see self::nodeAggregateId}
* @param OriginDimensionSpacePoint $originDimensionSpacePoint The DimensionSpacePoint the node originates in. Usually needed to address a Node in a NodeAggregate in order to update it.
* @param NodeAggregateClassification $classification The classification (regular, root, tethered) of this node
* @param NodeTypeName $nodeTypeName The node's node type name; always set, even if unknown to the NodeTypeManager
* @param NodeType|null $nodeType The node's node type, null if unknown to the NodeTypeManager - @deprecated Don't rely on this too much, as the capabilities of the NodeType here will probably change a lot; Ask the {@see NodeTypeManager} instead
- * @param PropertyCollection $properties All properties of this node. References are NOT part of this API; To access references, {@see ContentSubgraphInterface::findReferences()} can be used; To read the serialized properties, call properties->serialized().
- * @param NodeName|null $nodeName The optional name of this node {@see ContentSubgraphInterface::findChildNodeConnectedThroughEdgeName()}
+ * @param PropertyCollection $properties All properties of this node. References are NOT part of this API; To access references, {@see ContentSubgraphInterface::findReferences()} can be used; To read the serialized properties use {@see PropertyCollection::serialized()}.
+ * @param NodeName|null $nodeName The optionally named hierarchy relation to the node's parent.
* @param Timestamps $timestamps Creation and modification timestamps of this node
*/
private function __construct(
@@ -69,10 +71,7 @@ public static function create(ContentSubgraphIdentity $subgraphIdentity, NodeAgg
}
/**
- * Returns the specified property.
- *
- * If the node has a content object attached, the property will be fetched
- * there if it is gettable.
+ * Returns the specified property, or null if it does not exist (or was set to null -> unset)
*
* @param string $propertyName Name of the property
* @return mixed value of the property
@@ -80,14 +79,15 @@ public static function create(ContentSubgraphIdentity $subgraphIdentity, NodeAgg
*/
public function getProperty(string $propertyName): mixed
{
- return $this->properties[$propertyName];
+ return $this->properties->offsetGet($propertyName);
}
/**
- * If this node has a property with the given name. Does NOT check the NodeType; but checks
- * for a non-NULL property value.
+ * If this node has a property with the given name. It does not check if the property exists in the current NodeType schema.
+ *
+ * That means that {@see self::getProperty()} will not be null, except for the rare case the property deserializing returns null.
*
- * @param string $propertyName
+ * @param string $propertyName Name of the property
* @return boolean
* @api
*/
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php
index 147f434598e..a6c755f109a 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php
@@ -17,13 +17,23 @@
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
/**
- * The relative node path is a collection of NodeNames. If it contains no elements, it is considered root.
+ * The relative node path is a collection of node names {@see NodeName}. If it contains no elements, it is considered root.
*
* Example:
* root path: '' is resolved to []
- * non-root path: 'my/site' is resolved to ~ ['my', 'site']
+ * non-root path: 'my-document/main' is resolved to ~ ['my-document', 'main']
*
* It describes the hierarchy path of a node to an ancestor node in a subgraph.
+ *
+ * To fetch a node on a path use the subgraph: {@see ContentSubgraphInterface::findNodeByPath()}
+ *
+ * ```php
+ * $subgraph->findNodeByPath(
+ * NodePath::fromString("my-document/main"),
+ * $siteNodeAggregateId
+ * )
+ * ```
+ *
* @api
*/
final class NodePath implements \JsonSerializable
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/PropertyCollection.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/PropertyCollection.php
index f673a3cfc5d..5c736d72a24 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/PropertyCollection.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/PropertyCollection.php
@@ -24,7 +24,7 @@
* @implements \IteratorAggregate
* @api This object should not be instantiated by 3rd parties, but it is part of the {@see Node} read model
*/
-final class PropertyCollection implements \ArrayAccess, \IteratorAggregate
+final class PropertyCollection implements \ArrayAccess, \IteratorAggregate, \Countable
{
/**
* Properties from Nodes
@@ -56,14 +56,16 @@ public function offsetExists($offset): bool
public function offsetGet($offset): mixed
{
- if (!isset($this->deserializedPropertyValuesRuntimeCache[$offset])) {
- $serializedProperty = $this->serializedPropertyValues->getProperty($offset);
- $this->deserializedPropertyValuesRuntimeCache[$offset] = $serializedProperty === null
- ? null
- : $this->propertyConverter->deserializePropertyValue($serializedProperty);
+ if (array_key_exists($offset, $this->deserializedPropertyValuesRuntimeCache)) {
+ return $this->deserializedPropertyValuesRuntimeCache[$offset];
}
- return $this->deserializedPropertyValuesRuntimeCache[$offset];
+ $serializedProperty = $this->serializedPropertyValues->getProperty($offset);
+ if ($serializedProperty === null) {
+ return null;
+ }
+ return $this->deserializedPropertyValuesRuntimeCache[$offset] =
+ $this->propertyConverter->deserializePropertyValue($serializedProperty);
}
public function offsetSet($offset, $value): never
@@ -90,4 +92,9 @@ public function serialized(): SerializedPropertyValues
{
return $this->serializedPropertyValues;
}
+
+ public function count(): int
+ {
+ return count($this->serializedPropertyValues);
+ }
}
diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamProjection.php b/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamProjection.php
index 74b9ab9907a..8c568ad961a 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamProjection.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamProjection.php
@@ -16,8 +16,12 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Comparator;
use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\SchemaConfig;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId;
@@ -34,6 +38,7 @@
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased;
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
+use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory;
use Neos\ContentRepository\Core\Projection\ProjectionInterface;
use Neos\ContentRepository\Core\Projection\ProjectionStateInterface;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
@@ -89,22 +94,17 @@ private function setupTables(): void
$connection->executeStatement(sprintf("UPDATE %s SET state='FORKED' WHERE state='REBASING'; ", $this->tableName));
}
- $schema = new Schema();
- $contentStreamTable = $schema->createTable($this->tableName);
- $contentStreamTable->addColumn('contentStreamId', Types::STRING)
- ->setLength(40)
- ->setNotnull(true);
- $contentStreamTable->addColumn('version', Types::INTEGER)
- ->setNotnull(true);
- $contentStreamTable->addColumn('sourceContentStreamId', Types::STRING)
- ->setLength(40)
- ->setNotnull(false);
- $contentStreamTable->addColumn('state', Types::STRING)
- ->setLength(20)
- ->setNotnull(true);
- $contentStreamTable->addColumn('removed', Types::BOOLEAN)
- ->setDefault(false)
- ->setNotnull(false);
+ $schema = DbalSchemaFactory::createSchemaWithTables($schemaManager, [
+ (new Table($this->tableName, [
+ DbalSchemaFactory::columnForContentStreamId('contentStreamId')->setNotnull(true),
+ (new Column('version', Type::getType(Types::INTEGER)))->setNotnull(true),
+ DbalSchemaFactory::columnForContentStreamId('sourceContentStreamId')->setNotnull(false),
+ // Should become a DB ENUM (unclear how to configure with DBAL) or int (latter needs adaption to code)
+ (new Column('state', Type::getType(Types::BINARY)))->setLength(20)->setNotnull(true),
+ (new Column('removed', Type::getType(Types::BOOLEAN)))->setDefault(false)->setNotnull(false)
+ ]))
+ ]);
+
$schemaDiff = (new Comparator())->compare($schemaManager->createSchema(), $schema);
foreach ($schemaDiff->toSaveSql($connection->getDatabasePlatform()) as $statement) {
$connection->executeStatement($statement);
diff --git a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php
index 3b83ff11b60..8fe377c4054 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php
@@ -16,8 +16,10 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Comparator;
-use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked;
@@ -25,6 +27,7 @@
use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled;
use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled;
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
+use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory;
use Neos\ContentRepository\Core\Projection\ProjectionInterface;
use Neos\EventStore\CatchUp\CheckpointStorageInterface;
use Neos\EventStore\DoctrineAdapter\DoctrineCheckpointStorage;
@@ -65,27 +68,20 @@ private function setupTables(): void
if (!$schemaManager instanceof AbstractSchemaManager) {
throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914);
}
- $schema = new Schema();
- $contentStreamTable = $schema->createTable($this->tableName);
- $contentStreamTable->addColumn('contentstreamid', Types::STRING)
- ->setLength(40)
- ->setNotnull(true);
- $contentStreamTable->addColumn('nodeaggregateid', Types::STRING)
- ->setLength(64)
- ->setNotnull(true);
- $contentStreamTable->addColumn('dimensionspacepointhash', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $contentStreamTable->addColumn('dimensionspacepoint', Types::TEXT)
- ->setNotnull(false);
- $contentStreamTable->addColumn('hidden', Types::BOOLEAN)
- ->setDefault(false)
- ->setNotnull(false);
-
- $contentStreamTable->setPrimaryKey(
+
+ $nodeHiddenStateTable = new Table($this->tableName, [
+ DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotNull(true),
+ DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotNull(false),
+ DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotNull(false),
+ DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotNull(false),
+ (new Column('hidden', Type::getType(Types::BOOLEAN)))->setDefault(false)->setNotnull(false)
+ ]);
+ $nodeHiddenStateTable->setPrimaryKey(
['contentstreamid', 'nodeaggregateid', 'dimensionspacepointhash']
);
+ $schema = DbalSchemaFactory::createSchemaWithTables($schemaManager, [$nodeHiddenStateTable]);
+
$schemaDiff = (new Comparator())->compare($schemaManager->createSchema(), $schema);
foreach ($schemaDiff->toSaveSql($connection->getDatabasePlatform()) as $statement) {
$connection->executeStatement($statement);
diff --git a/Neos.ContentRepository.Core/Classes/Projection/Workspace/WorkspaceProjection.php b/Neos.ContentRepository.Core/Classes/Projection/Workspace/WorkspaceProjection.php
index 35a1834ad11..85ed95bbc4d 100644
--- a/Neos.ContentRepository.Core/Classes/Projection/Workspace/WorkspaceProjection.php
+++ b/Neos.ContentRepository.Core/Classes/Projection/Workspace/WorkspaceProjection.php
@@ -16,8 +16,10 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Comparator;
-use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated;
@@ -33,6 +35,7 @@
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased;
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
+use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory;
use Neos\ContentRepository\Core\Projection\ProjectionInterface;
use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
@@ -48,6 +51,8 @@
*/
class WorkspaceProjection implements ProjectionInterface, WithMarkStaleInterface
{
+ private const DEFAULT_TEXT_COLLATION = 'utf8mb4_unicode_520_ci';
+
/**
* @var WorkspaceFinder|null Cache for the workspace finder returned by {@see getState()},
* so that always the same instance is returned
@@ -82,32 +87,18 @@ private function setupTables(): void
throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914);
}
- $schema = new Schema();
- $workspaceTable = $schema->createTable($this->tableName);
- $workspaceTable->addColumn('workspacename', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $workspaceTable->addColumn('baseworkspacename', Types::STRING)
- ->setLength(255)
- ->setNotnull(false);
- $workspaceTable->addColumn('workspacetitle', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $workspaceTable->addColumn('workspacedescription', Types::STRING)
- ->setLength(255)
- ->setNotnull(true);
- $workspaceTable->addColumn('workspaceowner', Types::STRING)
- ->setLength(255)
- ->setNotnull(false);
- $workspaceTable->addColumn('currentcontentstreamid', Types::STRING)
- ->setLength(40)
- ->setNotnull(false);
- $workspaceTable->addColumn('status', Types::STRING)
- ->setLength(50)
- ->setNotnull(false);
-
+ $workspaceTable = new Table($this->tableName, [
+ (new Column('workspacename', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('baseworkspacename', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('workspacetitle', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('workspacedescription', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ (new Column('workspaceowner', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION),
+ DbalSchemaFactory::columnForContentStreamId('currentcontentstreamid')->setNotNull(true),
+ (new Column('status', Type::getType(Types::STRING)))->setLength(20)->setNotnull(false)->setCustomSchemaOption('charset', 'binary')
+ ]);
$workspaceTable->setPrimaryKey(['workspacename']);
+ $schema = DbalSchemaFactory::createSchemaWithTables($schemaManager, [$workspaceTable]);
$schemaDiff = (new Comparator())->compare($schemaManager->createSchema(), $schema);
foreach ($schemaDiff->toSaveSql($connection->getDatabasePlatform()) as $statement) {
$connection->executeStatement($statement);
diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeName.php b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeName.php
index 017ab717eba..5d64588e5ec 100644
--- a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeName.php
+++ b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeName.php
@@ -15,13 +15,25 @@
namespace Neos\ContentRepository\Core\SharedModel\Node;
use Behat\Transliterator\Transliterator;
+use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
/**
- * The Node name is the "path part" of the node; i.e. when accessing the node "/foo" via path,
+ * The Node name is the "path part" of the node; i.e. when accessing the node "/foo" via path {@see NodePath},
* the node name is "foo".
*
* Semantically it describes the hierarchical relation of a node to its parent, e.g. "main" denotes the main child node.
*
+ * Multiple node names describe a node path {@see NodePath}
+ *
+ * To fetch the child node that is connected with the parent via the name "main" use the subgraph's: {@see ContentSubgraphInterface::findNodeByPath()}
+ *
+ * ```php
+ * $subgraph->findNodeByPath(
+ * NodeName::fromString("main"),
+ * $parentNodeAggregateId
+ * )
+ * ```
+ *
* @api
*/
final class NodeName implements \JsonSerializable
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php
index f96db3b58cc..808ce0f40f7 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php
+++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php
@@ -62,7 +62,12 @@ public function run(): ProcessorResult
continue;
}
foreach ($properties as $propertyName => $propertyValue) {
- $propertyType = $nodeType->getPropertyType($propertyName);
+ try {
+ $propertyType = $nodeType->getPropertyType($propertyName);
+ } catch (\InvalidArgumentException $exception) {
+ $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']);
+ continue;
+ }
foreach ($this->extractAssetIdentifiers($propertyType, $propertyValue) as $assetId) {
if (array_key_exists($assetId, $this->processedAssetIds)) {
continue;
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php
index cc3ad13609b..4ea8a74d16f 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php
+++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php
@@ -316,7 +316,12 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType
}
foreach ($decodedProperties as $propertyName => $propertyValue) {
- $type = $nodeType->getPropertyType($propertyName);
+ try {
+ $type = $nodeType->getPropertyType($propertyName);
+ } catch (\InvalidArgumentException $exception) {
+ $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']);
+ continue;
+ }
if ($type === 'reference' || $type === 'references') {
if (!empty($propertyValue)) {
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php
index 0054ab25685..ac0db3ac9dc 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php
+++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php
@@ -62,6 +62,11 @@ class FeatureContext implements Context
*/
private array $loggedErrors = [];
+ /**
+ * @var array
+ */
+ private array $loggedWarnings = [];
+
private ContentRepository $contentRepository;
protected ContentRepositoryRegistry $contentRepositoryRegistry;
@@ -141,6 +146,8 @@ public function iRunTheEventMigration(string $contentStream = null): void
$migration->onMessage(function (Severity $severity, string $message) {
if ($severity === Severity::ERROR) {
$this->loggedErrors[] = $message;
+ } elseif ($severity === Severity::WARNING) {
+ $this->loggedWarnings[] = $message;
}
});
$this->lastMigrationResult = $migration->run();
@@ -186,12 +193,21 @@ public function iExpectTheFollowingEventsToBeExported(TableNode $table): void
/**
* @Then I expect the following errors to be logged
*/
- public function iExpectTheFollwingErrorsToBeLogged(TableNode $table): void
+ public function iExpectTheFollowingErrorsToBeLogged(TableNode $table): void
{
Assert::assertSame($table->getColumn(0), $this->loggedErrors, 'Expected logged errors do not match');
$this->loggedErrors = [];
}
+ /**
+ * @Then I expect the following warnings to be logged
+ */
+ public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void
+ {
+ Assert::assertSame($table->getColumn(0), $this->loggedWarnings, 'Expected logged warnings do not match');
+ $this->loggedWarnings = [];
+ }
+
/**
* @Then I expect a MigrationError
* @Then I expect a MigrationError with the message
@@ -307,6 +323,8 @@ public function findAssetById(string $assetId): SerializedAsset|SerializedImageV
$migration->onMessage(function (Severity $severity, string $message) {
if ($severity === Severity::ERROR) {
$this->loggedErrors[] = $message;
+ } elseif ($severity === Severity::WARNING) {
+ $this->loggedWarnings[] = $message;
}
});
$this->lastMigrationResult = $migration->run();
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature
index f4ca3e07455..52ac42afe7e 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature
+++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature
@@ -43,9 +43,9 @@ Feature: Export of used Assets, Image Variants and Persistent Resources
Scenario: Exporting an Image Variant includes the original Image asset as well
When I have the following node data rows:
- | Identifier | Path | Node Type | Properties |
- | sites-node-id | /sites | unstructured | |
- | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/variant1"} |
+ | Identifier | Path | Node Type | Properties |
+ | sites-node-id | /sites | unstructured | |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/variant1"} |
And I run the asset migration
Then I expect the following Assets to be exported:
"""
@@ -87,12 +87,12 @@ Feature: Export of used Assets, Image Variants and Persistent Resources
Scenario: Assets and image variants are only exported once each
When I have the following node data rows:
- | Identifier | Path | Node Type | Dimension Values | Properties |
- | sites-node-id | /sites | unstructured | | |
- | site-node-id | /sites/test-site | Some.Package:Homepage | | {"string": "asset:\/\/asset1"} |
- | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | {"image": {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}} |
- | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["en"]} | {"assets": [{"__flow_object_type": "Neos\\Media\\Domain\\Model\\Document", "__identifier": "asset3"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\ImageVariant", "__identifier": "variant1"}]} |
- | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | {"string": "some text with an asset link "} |
+ | Identifier | Path | Node Type | Dimension Values | Properties |
+ | sites-node-id | /sites | unstructured | | |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | | {"string": "asset:\/\/asset1"} |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | {"image": {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}} |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["en"]} | {"assets": [{"__flow_object_type": "Neos\\Media\\Domain\\Model\\Document", "__identifier": "asset3"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\ImageVariant", "__identifier": "variant1"}]} |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | {"string": "some text with an asset link "} |
And I run the asset migration
Then I expect the following Assets to be exported:
"""
@@ -178,12 +178,22 @@ Feature: Export of used Assets, Image Variants and Persistent Resources
Scenario: Referring to non-existing asset
When I have the following node data rows:
- | Identifier | Path | Node Type | Properties |
- | sites-node-id | /sites | unstructured | |
- | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/non-existing-asset"} |
+ | Identifier | Path | Node Type | Properties |
+ | sites-node-id | /sites | unstructured | |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/non-existing-asset"} |
And I run the asset migration
Then I expect no Assets to be exported
And I expect no ImageVariants to be exported
And I expect no PersistentResources to be exported
And I expect the following errors to be logged
| Failed to extract assets of property "string" of node "site-node-id" (type: "Some.Package:Homepage"): Failed to find mock asset with id "non-existing-asset" |
+
+ Scenario: Nodes with properties that are not part of the node type schema (see https://github.com/neos/neos-development-collection/issues/4804)
+ When I have the following node data rows:
+ | Identifier | Path | Node Type | Properties |
+ | sites-node-id | /sites | unstructured | |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"unknownProperty": "asset:\/\/variant1"} |
+ And I run the asset migration
+ Then I expect no Assets to be exported
+ And I expect the following warnings to be logged
+ | Skipped node data processing for the property "unknownProperty". The property name is not part of the NodeType schema for the NodeType "Some.Package:Homepage". (Node: site-node-id) |
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature
index 52e659f6017..8cd77523574 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature
+++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature
@@ -36,12 +36,12 @@ Feature: Migrations that contain nodes with "reference" or "references propertie
| c | /sites/site/c | Some.Package:SomeNodeType | {"text": "This is c", "refs": ["a", "b"]} |
And I run the event migration
Then I expect the following events to be exported
- | Type | Payload |
- | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "c"} |
+ | Type | Payload |
+ | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "c"} |
| NodeReferencesWereSet | {"sourceNodeAggregateId":"a","affectedSourceOriginDimensionSpacePoints":[[]],"referenceName":"ref","references":{"b":{"targetNodeAggregateId":"b","properties":null}}} |
| NodeReferencesWereSet | {"sourceNodeAggregateId":"c","affectedSourceOriginDimensionSpacePoints":[[]],"referenceName":"refs","references":{"a":{"targetNodeAggregateId":"a","properties":null},"b":{"targetNodeAggregateId":"b","properties":null}}} |
@@ -60,13 +60,26 @@ Feature: Migrations that contain nodes with "reference" or "references propertie
| c | /sites/site/c | Some.Package:SomeNodeType | {"language": ["ch"]} | {"text": "This is c", "refs": ["a", "b"]} |
And I run the event migration
Then I expect the following events to be exported
- | Type | Payload |
- | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site"} |
- | NodePeerVariantWasCreated | {} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b"} |
- | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "c"} |
+ | Type | Payload |
+ | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site"} |
+ | NodePeerVariantWasCreated | {} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "c"} |
| NodeReferencesWereSet | {"sourceNodeAggregateId":"a","affectedSourceOriginDimensionSpacePoints":[{"language": "en"}],"referenceName":"ref","references":{"b":{"targetNodeAggregateId":"b","properties":null}}} |
| NodeReferencesWereSet | {"sourceNodeAggregateId":"b","affectedSourceOriginDimensionSpacePoints":[{"language": "de"}],"referenceName":"ref","references":{"a":{"targetNodeAggregateId":"a","properties":null}}} |
| NodeReferencesWereSet | {"sourceNodeAggregateId":"c","affectedSourceOriginDimensionSpacePoints":[{"language": "ch"}],"referenceName":"refs","references":{"a":{"targetNodeAggregateId":"a","properties":null},"b":{"targetNodeAggregateId":"b","properties":null}}} |
+
+ Scenario: Nodes with properties that are not part of the node type schema (see https://github.com/neos/neos-development-collection/issues/4804)
+ When I have the following node data rows:
+ | Identifier | Path | Node Type | Properties |
+ | sites-node-id | /sites | unstructured | |
+ | site-node-id | /sites/test-site | Some.Package:Homepage | {"unknownProperty": "ref"} |
+ And I run the event migration
+ Then I expect the following events to be exported
+ | Type | Payload |
+ | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id"} |
+ | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id"} |
+ And I expect the following warnings to be logged
+ | Skipped node data processing for the property "unknownProperty". The property name is not part of the NodeType schema for the NodeType "Some.Package:Homepage". (Node: site-node-id) |
diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php
index c9bfd37be60..cce0d558a0a 100644
--- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php
+++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php
@@ -12,6 +12,7 @@
*/
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
+use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\NodeType\NodeTypeNames;
@@ -136,18 +137,13 @@ protected function earlyOptimizationOfFilters(FlowQuery $flowQuery, array $parse
// Optimize property name filter if present
if (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) {
$nodePath = $filter['PropertyNameFilter'] ?? $filter['PathFilter'];
- $nodePathSegments = explode('/', $nodePath);
/** @var Node $contextNode */
foreach ($flowQuery->getContext() as $contextNode) {
- $currentPathSegments = $nodePathSegments;
- $resolvedNode = $contextNode;
- while (($nodePathSegment = array_shift($currentPathSegments)) && !is_null($resolvedNode)) {
- $resolvedNode = $this->contentRepositoryRegistry->subgraphForNode($resolvedNode)
- ->findChildNodeConnectedThroughEdgeName(
- $resolvedNode->nodeAggregateId,
- NodeName::fromString($nodePathSegment)
+ $resolvedNode = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
+ ->findNodeByPath(
+ NodePath::fromString($nodePath),
+ $contextNode->nodeAggregateId,
);
- }
if (!is_null($resolvedNode) && !isset($filteredOutputNodeIdentifiers[
$resolvedNode->nodeAggregateId->value
diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php
index 1484827576b..405fd46196f 100644
--- a/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php
+++ b/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php
@@ -53,10 +53,9 @@ public function execute(
DimensionSpacePointSet $coveredDimensionSpacePoints,
ContentStreamId $contentStreamForWriting
): ?CommandResult {
- if ($node->hasProperty($this->propertyName)) {
- $properties = $node->properties;
- /** @var SerializedPropertyValue $serializedPropertyValue safe since Node::hasProperty */
- $serializedPropertyValue = $properties->serialized()->getProperty($this->propertyName);
+ $properties = $node->properties->serialized();
+ if ($properties->propertyExists($this->propertyName)) {
+ $serializedPropertyValue = $properties->getProperty($this->propertyName);
$propertyValue = $serializedPropertyValue->value;
if (!is_string($propertyValue)) {
throw new \Exception(
diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php
index 2883857ff9b..c4dc2eebf33 100644
--- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php
+++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php
@@ -11,6 +11,7 @@
use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate;
+use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved;
@@ -73,9 +74,9 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat
$originDimensionSpacePoint->toDimensionSpacePoint(),
VisibilityConstraints::withoutRestrictions()
);
- $tetheredNode = $subgraph->findChildNodeConnectedThroughEdgeName(
+ $tetheredNode = $subgraph->findNodeByPath(
+ $tetheredNodeName,
$nodeAggregate->nodeAggregateId,
- $tetheredNodeName
);
if ($tetheredNode === null) {
$foundMissingOrDisallowedTetheredNodes = true;
diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php
index 6013ed435f0..b4e6246665b 100644
--- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php
+++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php
@@ -21,6 +21,7 @@
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate;
+use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\ContentRepository\Core\SharedModel\User\UserId;
@@ -161,9 +162,9 @@ public function getCurrentSubgraph(): ContentSubgraphInterface
*/
public function iRememberNodeAggregateIdOfNodesChildAs(string $parentNodeAggregateId, string $childNodeName, string $indexName): void
{
- $this->rememberedNodeAggregateIds[$indexName] = $this->getCurrentSubgraph()->findChildNodeConnectedThroughEdgeName(
+ $this->rememberedNodeAggregateIds[$indexName] = $this->getCurrentSubgraph()->findNodeByPath(
+ NodePath::fromString($childNodeName),
NodeAggregateId::fromString($parentNodeAggregateId),
- NodeName::fromString($childNodeName)
)->nodeAggregateId;
}
diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php
index 304d9cab357..48335274b84 100644
--- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php
+++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php
@@ -80,15 +80,13 @@ trait CRTestSuiteTrait
use WorkspaceDiscarding;
use WorkspacePublishing;
- protected function setupCRTestSuiteTrait(bool $alwaysRunCrSetup = false): void
+ protected function setupCRTestSuiteTrait(): void
{
if (getenv('CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION')) {
CatchUpTriggerWithSynchronousOption::enableSynchronicityForSpeedingUpTesting();
}
}
- private static bool $wasContentRepositorySetupCalled = false;
-
/**
* @BeforeScenario
* @throws \Exception
diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php
index 8f9680a2736..32e5de9f0fd 100644
--- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php
+++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php
@@ -220,7 +220,8 @@ public function eventNumberIs(int $eventNumber, string $eventType, TableNode $pa
$key = $assertionTableRow['Key'];
$actualValue = Arrays::getValueByPath($actualEventPayload, $key);
- if ($key === 'affectedDimensionSpacePoints') {
+ // Note: For dimension space points we switch to an array comparison because the order is not deterministic (@see https://github.com/neos/neos-development-collection/issues/4769)
+ if ($key === 'affectedDimensionSpacePoints' || $key === 'affectedOccupiedDimensionSpacePoints') {
$expected = DimensionSpacePointSet::fromJsonString($assertionTableRow['Expected']);
$actual = DimensionSpacePointSet::fromArray($actualValue);
Assert::assertTrue($expected->equals($actual), 'Actual Dimension Space Point set "' . json_encode($actualValue) . '" does not match expected Dimension Space Point set "' . $assertionTableRow['Expected'] . '"');
diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php
index 1e0323c69d2..168a6f7a055 100644
--- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php
+++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php
@@ -157,16 +157,16 @@ public function iExecuteTheFindNodeByAbsolutePathQueryIExpectTheFollowingNodes(s
}
/**
- * @When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id :parentNodeIdSerialized and edge name :edgeNameSerialized I expect no node to be returned
- * @When I execute the findChildNodeConnectedThroughEdgeName query for parent node aggregate id :parentNodeIdSerialized and edge name :edgeNameSerialized I expect the node :expectedNodeIdSerialized to be returned
+ * @When I execute the findNodeByPath query for parent node aggregate id :parentNodeIdSerialized and node name :edgeNameSerialized as path I expect no node to be returned
+ * @When I execute the findNodeByPath query for parent node aggregate id :parentNodeIdSerialized and node name :edgeNameSerialized as path I expect the node :expectedNodeIdSerialized to be returned
*/
- public function iExecuteTheFindChildNodeConnectedThroughEdgeNameQueryIExpectTheFollowingNodes(string $parentNodeIdSerialized, string $edgeNameSerialized, string $expectedNodeIdSerialized = null): void
+ public function iExecuteTheFindChildNodeByNodeNameQueryIExpectTheFollowingNodes(string $parentNodeIdSerialized, string $edgeNameSerialized, string $expectedNodeIdSerialized = null): void
{
$parentNodeAggregateId = NodeAggregateId::fromString($parentNodeIdSerialized);
$edgeName = NodeName::fromString($edgeNameSerialized);
$expectedNodeAggregateId = $expectedNodeIdSerialized !== null ? NodeAggregateId::fromString($expectedNodeIdSerialized) : null;
- $actualNode = $this->getCurrentSubgraph()->findChildNodeConnectedThroughEdgeName($parentNodeAggregateId, $edgeName);
+ $actualNode = $this->getCurrentSubgraph()->findNodeByPath($edgeName, $parentNodeAggregateId);
Assert::assertSame($actualNode?->nodeAggregateId->value, $expectedNodeAggregateId?->value);
}
@@ -263,7 +263,8 @@ public function iExecuteTheFindDescendantNodesQueryIExpectTheFollowingNodes(stri
$subgraph = $this->getCurrentSubgraph();
$actualNodeIds = array_map(static fn(Node $node) => $node->nodeAggregateId->value, iterator_to_array($subgraph->findDescendantNodes($entryNodeAggregateId, $filter)));
- Assert::assertSame($expectedNodeIds, $actualNodeIds, 'findDescendantNodes returned an unexpected result');
+ // Note: In contrast to other similar checks, in this case we use assertEqualsCanonicalizing() instead of assertSame() because the order of descendant nodes is not completely deterministic (@see https://github.com/neos/neos-development-collection/issues/4769)
+ Assert::assertEqualsCanonicalizing($expectedNodeIds, $actualNodeIds, 'findDescendantNodes returned an unexpected result');
$actualCount = $subgraph->countDescendantNodes($entryNodeAggregateId, CountDescendantNodesFilter::fromFindDescendantNodesFilter($filter));
Assert::assertSame($expectedTotalCount ?? count($expectedNodeIds), $actualCount, 'countDescendantNodes returned an unexpected result');
}
diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php
index e0abd0e1d3c..5ba244abcd9 100644
--- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php
+++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php
@@ -456,7 +456,7 @@ public function iExpectThisNodeToBeTheChildOfNode(string $serializedParentNodeDi
Assert::assertTrue($expectedParentDiscriminator->equals($actualParentDiscriminator), 'Parent discriminator does not match. Expected was ' . json_encode($expectedParentDiscriminator) . ', given was ' . json_encode($actualParentDiscriminator));
$expectedChildDiscriminator = NodeDiscriminator::fromNode($currentNode);
- $child = $subgraph->findChildNodeConnectedThroughEdgeName($parent->nodeAggregateId, $currentNode->nodeName);
+ $child = $subgraph->findNodeByPath($currentNode->nodeName, $parent->nodeAggregateId);
$actualChildDiscriminator = NodeDiscriminator::fromNode($child);
Assert::assertTrue($expectedChildDiscriminator->equals($actualChildDiscriminator), 'Child discriminator does not match. Expected was ' . json_encode($expectedChildDiscriminator) . ', given was ' . json_encode($actualChildDiscriminator));
});
diff --git a/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php b/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php
index 104fe983c8a..6bacd50a538 100644
--- a/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php
+++ b/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php
@@ -34,7 +34,7 @@ trait CRRegistrySubjectProvider
/**
* @var array
*/
- protected array $alreadySetUpContentRepositories = [];
+ protected static array $alreadySetUpContentRepositories = [];
/**
* @template T of object
@@ -62,8 +62,9 @@ public function iInitializeContentRepository(string $contentRepositoryId): void
$eventTableName = sprintf('cr_%s_events', $contentRepositoryId);
$databaseConnection->executeStatement('TRUNCATE ' . $eventTableName);
- if (!in_array($contentRepository->id, $this->alreadySetUpContentRepositories)) {
+ if (!in_array($contentRepository->id, self::$alreadySetUpContentRepositories)) {
$contentRepository->setUp();
+ self::$alreadySetUpContentRepositories[] = $contentRepository->id;
}
$contentRepository->resetProjectionStates();
}
diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml
index bab78f378aa..650abaaff54 100644
--- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml
+++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml
@@ -1,5 +1,11 @@
Neos:
Flow:
+ persistence:
+ doctrine:
+ migrations:
+ ignoredTables:
+ 'cr_.*': true
+
# Improve debug output for node objects by ignoring large classes
error:
debugger:
diff --git a/Neos.Media.Browser/Classes/Controller/AssetController.php b/Neos.Media.Browser/Classes/Controller/AssetController.php
index 7f78d961061..f15304dfa1b 100644
--- a/Neos.Media.Browser/Classes/Controller/AssetController.php
+++ b/Neos.Media.Browser/Classes/Controller/AssetController.php
@@ -14,6 +14,7 @@
use Doctrine\Common\Persistence\Proxy as DoctrineProxy;
use Doctrine\ORM\EntityNotFoundException;
+use enshrined\svgSanitize\Sanitizer;
use Neos\Error\Messages\Error;
use Neos\Error\Messages\Message;
use Neos\Flow\Annotations as Flow;
@@ -35,6 +36,7 @@
use Neos\Media\Domain\Model\AssetCollection;
use Neos\Media\Domain\Model\AssetInterface;
use Neos\Media\Domain\Model\AssetSource\AssetNotFoundExceptionInterface;
+use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface;
use Neos\Media\Domain\Model\AssetSource\AssetProxyRepositoryInterface;
use Neos\Media\Domain\Model\AssetSource\AssetSourceConnectionExceptionInterface;
use Neos\Media\Domain\Model\AssetSource\AssetSourceInterface;
@@ -370,7 +372,8 @@ public function showAction(string $assetSourceIdentifier, string $assetProxyIden
$this->view->assignMultiple([
'assetProxy' => $assetProxy,
- 'assetCollections' => $this->assetCollectionRepository->findAll()
+ 'assetCollections' => $this->assetCollectionRepository->findAll(),
+ 'assetContainsMaliciousContent' => $this->checkForMaliciousContent($assetProxy)
]);
} catch (AssetNotFoundExceptionInterface | AssetSourceConnectionExceptionInterface $e) {
$this->view->assign('connectionError', $e);
@@ -423,6 +426,7 @@ public function editAction(string $assetSourceIdentifier, string $assetProxyIden
'assetCollections' => $this->assetCollectionRepository->findAll(),
'contentPreview' => $contentPreview,
'assetSource' => $assetSource,
+ 'assetContainsMaliciousContent' => $this->checkForMaliciousContent($assetProxy),
'canShowVariants' => ($assetProxy instanceof NeosAssetProxy) && ($assetProxy->getAsset() instanceof VariantSupportInterface)
]);
} catch (AssetNotFoundExceptionInterface | AssetSourceConnectionExceptionInterface $e) {
@@ -1022,4 +1026,25 @@ private function forwardWithConstraints(string $actionName, string $controllerNa
}
$this->forward($actionName, $controllerName, null, $arguments);
}
+
+ private function checkForMaliciousContent(AssetProxyInterface $assetProxy): bool
+ {
+ if ($assetProxy->getMediaType() == 'image/svg+xml') {
+ // @todo: Simplify again when https://github.com/darylldoyle/svg-sanitizer/pull/90 is merged and released.
+ $previousXmlErrorHandling = libxml_use_internal_errors(true);
+ $sanitizer = new Sanitizer();
+
+ $resource = stream_get_contents($assetProxy->getImportStream());
+
+ $sanitizer->sanitize($resource);
+ libxml_clear_errors();
+ libxml_use_internal_errors($previousXmlErrorHandling);
+ $issues = $sanitizer->getXmlIssues();
+ if ($issues && count($issues) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php
index 6a1c6b924fe..e53cfa0df5a 100644
--- a/Neos.Media.Browser/Classes/Controller/UsageController.php
+++ b/Neos.Media.Browser/Classes/Controller/UsageController.php
@@ -134,14 +134,14 @@ public function relatedNodesAction(AssetInterface $asset)
continue;
}
- $documentNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_DOCUMENT));
+ $documentNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT));
// this should actually never happen, too.
if (!$documentNode) {
$inaccessibleRelations[] = $inaccessibleRelation;
continue;
}
- $siteNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE));
+ $siteNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
// this should actually never happen, too. :D
if (!$siteNode) {
$inaccessibleRelations[] = $inaccessibleRelation;
diff --git a/Neos.Media.Browser/Resources/Private/Partials/ContentDefaultPreview.html b/Neos.Media.Browser/Resources/Private/Partials/ContentDefaultPreview.html
index f32baac89dd..3f0963cdf42 100644
--- a/Neos.Media.Browser/Resources/Private/Partials/ContentDefaultPreview.html
+++ b/Neos.Media.Browser/Resources/Private/Partials/ContentDefaultPreview.html
@@ -1,7 +1,14 @@
{namespace m=Neos\Media\ViewHelpers}
{namespace neos=Neos\Neos\ViewHelpers}
diff --git a/Neos.Media.Browser/Resources/Private/Templates/Asset/Edit.html b/Neos.Media.Browser/Resources/Private/Templates/Asset/Edit.html
index f40c47e7df1..06ec12fa5d7 100644
--- a/Neos.Media.Browser/Resources/Private/Templates/Asset/Edit.html
+++ b/Neos.Media.Browser/Resources/Private/Templates/Asset/Edit.html
@@ -78,7 +78,19 @@ {neos:backend.translate(id: 'connectionError', package: 'Neos.Media.Browser'
{neos:backend.translate(id: 'metadata.filename', package: 'Neos.Media.Browser')}
- {assetProxy.filename}
+
+
+
+ {assetProxy.filename}
+
+ {neos:backend.translate(id: 'message.assetContainsMaliciousContent', package: 'Neos.Media.Browser')}
+
+
+
+ {assetProxy.filename}
+
+
+
{neos:backend.translate(id: 'metadata.lastModified', package: 'Neos.Media.Browser')}
diff --git a/Neos.Media.Browser/Resources/Private/Templates/Asset/Show.html b/Neos.Media.Browser/Resources/Private/Templates/Asset/Show.html
index 0375e584638..c74719983bb 100644
--- a/Neos.Media.Browser/Resources/Private/Templates/Asset/Show.html
+++ b/Neos.Media.Browser/Resources/Private/Templates/Asset/Show.html
@@ -39,7 +39,19 @@
{neos:backend.translate(id: 'metadata.filename', package: 'Neos.Media.Browser')}
- {assetProxy.filename}
+
+
+
+ {assetProxy.filename}
+
+ {neos:backend.translate(id: 'message.assetContainsMaliciousContent', package: 'Neos.Media.Browser')}
+
+
+
+ {assetProxy.filename}
+
+
+
{neos:backend.translate(id: 'metadata.lastModified', package: 'Neos.Media.Browser')}
@@ -85,9 +97,16 @@
{neos:backend.translate(id: 'preview', package: 'Neos.Media.Browser')}
diff --git a/Neos.Media.Browser/Resources/Private/Translations/en/Main.xlf b/Neos.Media.Browser/Resources/Private/Translations/en/Main.xlf
index 07f3354ed02..1dd0ef007a1 100644
--- a/Neos.Media.Browser/Resources/Private/Translations/en/Main.xlf
+++ b/Neos.Media.Browser/Resources/Private/Translations/en/Main.xlf
@@ -101,6 +101,9 @@
This operation cannot be undone.
+
+ This asset might contain malicious content!
+
Cancel
diff --git a/Neos.Media.Browser/Resources/Private/Translations/es/Main.xlf b/Neos.Media.Browser/Resources/Private/Translations/es/Main.xlf
index 1cadd286e74..213a5dfc92c 100644
--- a/Neos.Media.Browser/Resources/Private/Translations/es/Main.xlf
+++ b/Neos.Media.Browser/Resources/Private/Translations/es/Main.xlf
@@ -570,6 +570,10 @@
Create missing variants
Crear las variantes que faltan
+
+ This asset might contain malicious content!
+ ¡Este activo puede tener contenido malicioso!
+
Drag and drop an asset on a collection / tag to add them to it.
- Sleep een asset naar een collectie of tag om deze daaraan toe te voegen.
+ Sleep een asset naar een collectie of tag om deze daaraan toe te voegen.
+
Keep the filename "{0}"
- Het bestand "{0}" behouden
+ Het bestand "{0}" behouden
+
Media
- Media
+ Media
+
This module allows managing of media assets including pictures, videos, audio and documents.
- Deze module stelt u in staat media bestanden waaronder afbeeldingen, video's, audio bestanden en documenten te beheren.
+ Deze module stelt u in staat media bestanden waaronder afbeeldingen, video's, audio bestanden en documenten te beheren.
+
Name
- Naam
+ Naam
+
Title
- Titel
+ Titel
+
Label
- Label
+ Label
+
Caption
- Onderschrift
+ Onderschrift
+
Copyright Notice
- Copyright melding
+ Copyright melding
+
Last modified
- Laatst bewerkt
+ Laatst bewerkt
+
File size
- Bestandsgrootte
+ Bestandsgrootte
+
Type
- Type
+ Type
+
Tags
- Tags
+ Tags
+
Sort by name
- Sorteer op naam
+ Sorteer op naam
+
Sort by last modified
- Sorteer op laatst gewijzigd
+ Sorteer op laatst gewijzigd
+
Sort by
- Sorteren op
+ Sorteren op
+
Sort direction
- Sorteer volgorde
+ Sorteer volgorde
+
Ascending
- Oplopend
+ Oplopend
+
Descending
- Aflopend
+ Aflopend
+
Sort direction Ascending
- Oplopend sorteren
+ Oplopend sorteren
+
Sort direction Descending
- Aflopend sorteren
+ Aflopend sorteren
+
Drag and drop on tag or collection
- Slepen en neerzetten op tag of collectie
+ Slepen en neerzetten op tag of collectie
+
View
- Weergeven
+ Weergeven
+
View asset
- Bekijk materiaal
+ Bekijk materiaal
+
Edit asset
- Bestand bewerken
+ Bestand bewerken
+
Delete asset
- Bestand verwijderen
+ Bestand verwijderen
+
Do you really want to delete asset "{0}"?
- Weet u zeker dat u bestand "{0}" wilt verwijderen?
+ Weet u zeker dat u bestand "{0}" wilt verwijderen?
+
Do you really want to delete collection "{0}"?
- Weet u zeker dat u collectie "{0}" wilt verwijderen?
+ Weet u zeker dat u collectie "{0}" wilt verwijderen?
+
Do you really want to delete tag "{0}"?
- Weet u zeker dat u tag "{0}" wilt verwijderen?
+ Weet u zeker dat u tag "{0}" wilt verwijderen?
+
This will delete the asset.
- Dit verwijderd het bestand.
+ Dit verwijderd het bestand.
+
This will delete the collection, but not the assets that it contains.
- Dit verwijderd de collectie, maar niet de bestanden die deze bevat.
+ Dit verwijderd de collectie, maar niet de bestanden die deze bevat.
+
This will delete the tag, but not the assets that has it.
- Dit verwijderd de tag, maar niet de bestanden die deze gebruikt.
+ Dit verwijderd de tag, maar niet de bestanden die deze gebruikt.
+
This operation cannot be undone.
- Deze bewerking kan niet ongedaan worden gemaakt.
+ Deze bewerking kan niet ongedaan worden gemaakt.
+
Cancel
- Annuleren
+ Annuleren
+
Replace
- Vervang
+ Vervang
+
Replace asset resource
- Vervang asset bron
+ Vervang asset bron
+
Save
- Sla op
+ Sla op
+
Yes, delete the asset
- Ja, verwijder het bestand
+ Ja, verwijder het bestand
+
Yes, delete the collection
- Ja, verwijder de collectie
+ Ja, verwijder de collectie
+
Yes, delete the tag
- Ja, verwijder de tag
+ Ja, verwijder de tag
+
Edit {0}
- Bewerk {0}
+ Bewerk {0}
+
Search in assets
- Zoek binnen de bestanden
+ Zoek binnen de bestanden
+
Search
- Zoeken
+ Zoeken
+
{0} items
- {0} items
+ {0} items
+
found matching "{0}"
- gevonden overeenkomende "{0}"
+ gevonden overeenkomende "{0}"
+
Upload
- Upload
+ Upload
+
Filter options
- Filter opties
+ Filter opties
+
Display all asset types
- Alle bestandstypen weergeven
+ Alle bestandstypen weergeven
+
Only display image assets
- Alleen afbeeldingen weergeven
+ Alleen afbeeldingen weergeven
+
Only display document assets
- Toon enkel documenten
+ Toon enkel documenten
+
Only display video assets
- Toon enkel videobestanden
+ Toon enkel videobestanden
+
Only display audio assets
- Toon enkel audiobestanden
+ Toon enkel audiobestanden
+
All
- Alle
+ Alle
+
Images
- Afbeeldingen
+ Afbeeldingen
+
Documents
- Documenten
+ Documenten
+
Video
- Video
+ Video
+
Audio
- Audio
+ Audio
+
Sort options
- Sorteeropties
+ Sorteeropties
+
List view
- Lijstweergave
+ Lijstweergave
+
Thumbnail view
- Miniatuurweergave
+ Miniatuurweergave
+
Connection error
- Verbindingsfout
+ Verbindingsfout
+
Media source
- Media bron
+ Media bron
+
Media sources
- Media bronnen
+ Media bronnen
+
Collections
- Collecties
+ Collecties
+
Edit collections
- Collecties bewerken
+ Collecties bewerken
+
Edit collection
- Collectie bewerken
+ Collectie bewerken
+
Delete collection
- Collectie verwijderen
+ Collectie verwijderen
+
Create collection
- Collectie aanmaken
+ Collectie aanmaken
+
Enter collection title
- Voer collectie titel in
+ Voer collectie titel in
+
All collections
- Alle collecties
+ Alle collecties
+
Tags
- Tags
+ Tags
+
Edit tags
- Bewerk tags
+ Bewerk tags
+
Edit tag
- Bewerk tag
+ Bewerk tag
+
Delete tag
- Verwijder tag
+ Verwijder tag
+
Enter tag label
- Voer het label van de tag in
+ Voer het label van de tag in
+
Create tag
- Maak tag aan
+ Maak tag aan
+
All assets
- Alle assets
+ Alle assets
+
All
- Alle
+ Alle
+
Untagged assets
- Bestanden zonder tag
+ Bestanden zonder tag
+
Untagged
- Zonder tag
+ Zonder tag
+
Max. upload size {0} per file
- Max. upload grootte {0} per bestand
+ Max. upload grootte {0} per bestand
+
Drop files here
- Drop bestanden hier
+ Drop bestanden hier
+
or click to upload
- of klik om te uploaden
+ of klik om te uploaden
+
Choose file
- Kies bestand
+ Kies bestand
+
No Assets found.
- Geen bestanden gevonden.
+ Geen bestanden gevonden.
+
Basics
- Basis
+ Basis
+
Delete
- Verwijderen
+ Verwijderen
+
Click to delete
- Klik om te verwijderen
+ Klik om te verwijderen
+
Save
- Sla op
+ Sla op
+
Metadata
- Metadata
+ Metadata
+
Filename
- Bestandsnaam
+ Bestandsnaam
+
Last modified (resource)
- Laatst bewerkt (resource)
+ Laatst bewerkt (resource)
+
File size
- Bestandsgrootte
+ Bestandsgrootte
+
Dimensions
- Dimensies
+ Dimensies
+
Type
- Type
+ Type
+
Identifier
- Identifier
+ Identifier
+
Title
- Titel
+ Titel
+
Caption
- Onderschrift
+ Onderschrift
+
Copyright Notice
- Copyright melding
+ Copyright melding
+
Preview
- Voorbeeld
+ Voorbeeld
+
Download
- Downloaden
+ Downloaden
+
Next
- Volgende
+ Volgende
+
Previous
- Vorige
+ Vorige
+
Cannot upload the file
- Kan het bestand niet uploaden
+ Kan het bestand niet uploaden
+
No file selected
- Geen bestand geselecteerd
+ Geen bestand geselecteerd
+
The file size of {0} exceeds the allowed limit of {1}
- De bestandsgrootte van {0} groter is dan de toegestane limiet van {1}
+ De bestandsgrootte van {0} groter is dan de toegestane limiet van {1}
+
for the file
- voor het bestand
+ voor het bestand
+
Only some of the files were successfully uploaded. Refresh the page to see the those.
- Slechts enkele van de bestanden zijn geüpload. Vernieuw de pagina om die te zien.
+ Slechts enkele van de bestanden zijn geüpload. Vernieuw de pagina om die te zien.
+
Tagging the asset failed.
- Taggen van bestand is mislukt.
+ Taggen van bestand is mislukt.
+
Adding the asset to the collection failed.
- Het toevoegen van het bestand aan de collectie is niet gelukt.
+ Het toevoegen van het bestand aan de collectie is niet gelukt.
+
Creating
- Wordt aangemaakt
+ Wordt aangemaakt
+
Asset could not be deleted, because there are still Nodes using it
- Bestand kon niet worden verwijderd, omdat het nog door nodes wordt gebruikt
+ Bestand kon niet worden verwijderd, omdat het nog door nodes wordt gebruikt
+
{0} usages
- {0} keer toegepast
-
+ {0} keer toegepast
{0} usages
- {0} keer toegepast
-
+ {0} keer toegepast
References to "{asset}"
- Referenties naar "{asset}"
+ Referenties naar "{asset}"
+
Replace "{filename}"
- Vervang "{filename}"
+ Vervang "{filename}"
+
You can replace this asset by uploading a new file. Once replaced, the new asset will be used on all places where the asset is used on your website.
- Je kunt dit bestand vervangen door een nieuw bestand te uploaden. Eenmaal vervangen, zal het nieuwe bestand worden gebruikt op alle plaatsen waar de asset op uw website wordt gebruikt.
+ Je kunt dit bestand vervangen door een nieuw bestand te uploaden. Eenmaal vervangen, zal het nieuwe bestand worden gebruikt op alle plaatsen waar de asset op uw website wordt gebruikt.
+
Note
- Notitie
+ Notitie
+
This operation will replace the asset in all published or unpublished workspaces, including the live website.
- Deze operatie vervangt de asset in alle gepubliceerde of ongepubliceerde werkruimten, inclusief de live website.
+ Deze operatie vervangt de asset in alle gepubliceerde of ongepubliceerde werkruimten, inclusief de live website.
+
Choose a new file
- Kies een nieuw bestand
+ Kies een nieuw bestand
+
Currently the asset is used {usageCount} times.
- Momenteel wordt de asset {usageCount} keer gebruikt.
+ Momenteel wordt de asset {usageCount} keer gebruikt.
+
Show all usages
- Toon alle toepassingen
+ Toon alle toepassingen
+
Currently the asset is not in used anywhere on the website.
- Momenteel wordt de asset nergens op de website gebruikt.
+ Momenteel wordt de asset nergens op de website gebruikt.
+
Preview current file
- Voorbeeld huidige bestand
+ Voorbeeld huidige bestand
+
Could not replace asset
- Kon het bestand niet vervangen
+ Kon het bestand niet vervangen
+
Asset "{0}" has been replaced.
- Bestand "{0}" is vervangen.
+ Bestand "{0}" is vervangen.
+
Asset "{0}" has been updated.
- Bestand "{0}" is aangepast.
+ Bestand "{0}" is aangepast.
+
Asset "{0}" has been added.
- Bestand "{0}" is toegevoegd.
+ Bestand "{0}" is toegevoegd.
+
Asset "{0}" has been deleted.
- Bestand "{0}" is verwijderd.
+ Bestand "{0}" is verwijderd.
+
Asset could not be deleted.
- Het bestand kon niet worden verwijderd.
+ Het bestand kon niet worden verwijderd.
+
Tag "{0}" already exists and was added to collection.
- Tag "{0}" bestond al en is toegevoegd aan de collectie.
+ Tag "{0}" bestond al en is toegevoegd aan de collectie.
+
Tag "{0}" has been created.
- Tag "{0}" is aangemaakt.
+ Tag "{0}" is aangemaakt.
+
Tag "{0}" has been updated.
- Tag "{0}" is aangepast.
+ Tag "{0}" is aangepast.
+
Tag "{0}" has been deleted.
- Tag "{0}" is verwijderd.
+ Tag "{0}" is verwijderd.
+
Collection "{0}" has been created.
- Collectie "{0}" is aangemaakt.
+ Collectie "{0}" is aangemaakt.
+
Collection "{0}" has been updated.
- Collectie "{0}" is aangepast.
+ Collectie "{0}" is aangepast.
+
Collection "{0}" has been deleted.
- Collectie "{0}" is verwijderd.
+ Collectie "{0}" is verwijderd.
+
Generate redirects from original file url to the new url
- Genereer een redirect van de originele locatie van het bestand naar de nieuwe locatie
+ Genereer een redirect van de originele locatie van het bestand naar de nieuwe locatie
+
'Resources of type "{0}" can only be replaced by a similar resource. Got type "{1}"'
- 'Bronnen van type "{0}' kunnen alleen worden vervangen door een vergelijkbare bron. Kreeg type "{1}"'
+ 'Bronnen van type "{0}' kunnen alleen worden vervangen door een vergelijkbare bron. Kreeg type "{1}"'
+
No access to workspace "{0}"
- Geen toegang tot werkplaats "{0}"
+ Geen toegang tot werkplaats "{0}"
+
No document node found for this node
- Geen documentnode gevonden voor deze node
+ Geen documentnode gevonden voor deze node
+
+
+ Create missing variants
+ Maak ontbrekende varianten aan
+