diff --git a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php index a2ae8df4..7abec4f6 100644 --- a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php +++ b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php @@ -18,7 +18,7 @@ class RelationFieldDefinitionMapperSpec extends ObjectBehavior function let(NameHelper $nameHelper, ContentTypeService $contentTypeService, FieldDefinitionMapper $innerMapper) { - $this->beConstructedWith($innerMapper, $nameHelper, $contentTypeService); + $this->beConstructedWith($innerMapper, $nameHelper, $contentTypeService, true); $articleContentType = new ContentType(['identifier' => 'article']); $folderContentType = new ContentType(['identifier' => 'folder']); @@ -56,19 +56,19 @@ function it_maps_single_selection_with_a_unique_type_limitations_to_a_single_ite function it_maps_multi_selection_without_type_limitations_to_an_array_of_generic_content() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, []); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_maps_multi_selection_with_multiple_type_limitations_to_an_array_of_generic_content() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_NONE, ['article', 'blog_post']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_maps_multi_selection_with_a_unique_type_limitations_to_an_array_of_that_type() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, ['article']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[ArticleItem]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } function it_delegates_the_field_definition_type_to_the_inner_mapper(FieldDefinitionMapper $innerMapper) @@ -81,13 +81,13 @@ function it_delegates_the_field_definition_type_to_the_inner_mapper(FieldDefinit function it_maps_multi_selection_to_resolve_multiple() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, true])'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, true, args])'); } function it_maps_single_selection_to_resolve_single() { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_SINGLE); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, false])'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=resolver("RelationFieldValue", [field, false, args])'); } private function createFieldDefinition($selectionLimit = 0, $selectionContentTypes = []) diff --git a/src/bundle/Resources/config/default_settings.yaml b/src/bundle/Resources/config/default_settings.yaml index b2de9003..d1189da0 100644 --- a/src/bundle/Resources/config/default_settings.yaml +++ b/src/bundle/Resources/config/default_settings.yaml @@ -1,5 +1,6 @@ parameters: ibexa.graphql.schema.should.extend.ezurl: false + ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination: false ibexa.graphql.schema.content.field_name.override: id: id_ ibexa.graphql.schema.content.mapping.field_definition_type: diff --git a/src/bundle/Resources/config/graphql/Field.types.yaml b/src/bundle/Resources/config/graphql/Field.types.yaml index f4138721..75115435 100644 --- a/src/bundle/Resources/config/graphql/Field.types.yaml +++ b/src/bundle/Resources/config/graphql/Field.types.yaml @@ -279,3 +279,13 @@ UrlFieldValue: type: String description: "The link's name or description" resolve: "@=value.text" + +RelationsConnection: + type: relay-connection + config: + nodeType: Item + connectionFields: + sliceSize: + type: Int! + orderBy: + type: String diff --git a/src/bundle/Resources/config/services/resolvers.yaml b/src/bundle/Resources/config/services/resolvers.yaml index 8b554697..3b708959 100644 --- a/src/bundle/Resources/config/services/resolvers.yaml +++ b/src/bundle/Resources/config/services/resolvers.yaml @@ -49,6 +49,8 @@ services: - { name: overblog_graphql.resolver, alias: "ItemsOfTypeAsConnection", method: "resolveItemsOfTypeAsConnection" } Ibexa\GraphQL\Resolver\RelationFieldResolver: + arguments: + $enablePagination: '%ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination%' tags: - { name: overblog_graphql.resolver, alias: "RelationFieldValue", method: "resolveRelationFieldValue" } diff --git a/src/bundle/Resources/config/services/schema.yaml b/src/bundle/Resources/config/services/schema.yaml index 14741e47..f58d7499 100644 --- a/src/bundle/Resources/config/services/schema.yaml +++ b/src/bundle/Resources/config/services/schema.yaml @@ -43,6 +43,7 @@ services: arguments: $contentTypeService: '@ibexa.siteaccessaware.service.content_type' $innerMapper: '@Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\RelationFieldDefinitionMapper.inner' + $enablePagination: '%ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination%' Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\SelectionFieldDefinitionMapper: decorates: Ibexa\Contracts\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper diff --git a/src/lib/Resolver/RelationFieldResolver.php b/src/lib/Resolver/RelationFieldResolver.php index f55dd3bc..5bb220dd 100644 --- a/src/lib/Resolver/RelationFieldResolver.php +++ b/src/lib/Resolver/RelationFieldResolver.php @@ -7,27 +7,38 @@ namespace Ibexa\GraphQL\Resolver; use GraphQL\Error\UserError; +use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Core\FieldType; use Ibexa\GraphQL\DataLoader\ContentLoader; use Ibexa\GraphQL\ItemFactory; +use Ibexa\GraphQL\Relay\PageAwareConnection; use Ibexa\GraphQL\Value\Field; +use Ibexa\GraphQL\Value\Item; +use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Relay\Connection\Paginator; final class RelationFieldResolver { - /** @var \Ibexa\GraphQL\DataLoader\ContentLoader */ - private $contentLoader; + public const DEFAULT_LIMIT = 25; - /** @var \Ibexa\GraphQL\ItemFactory */ - private $itemFactory; + private ContentLoader $contentLoader; - public function __construct(ContentLoader $contentLoader, ItemFactory $relatedContentItemFactory) - { + private ItemFactory $itemFactory; + + private bool $enablePagination; + + public function __construct( + ContentLoader $contentLoader, + ItemFactory $relatedContentItemFactory, + bool $enablePagination + ) { $this->contentLoader = $contentLoader; $this->itemFactory = $relatedContentItemFactory; + $this->enablePagination = $enablePagination; } - public function resolveRelationFieldValue(Field $field, $multiple = false) + public function resolveRelationFieldValue(Field $field, $multiple = false, ?Argument $args = null) { $destinationContentIds = $this->getContentIds($field); @@ -35,21 +46,53 @@ public function resolveRelationFieldValue(Field $field, $multiple = false) return $multiple ? [] : null; } - $contentItems = $this->contentLoader->find(new Query( + $query = new Query( ['filter' => new Query\Criterion\ContentId($destinationContentIds)] - )); + ); if ($multiple) { - return array_map( - function ($contentId) use ($contentItems) { - return $this->itemFactory->fromContent( - $contentItems[array_search($contentId, array_column($contentItems, 'id'))] - ); - }, - $destinationContentIds + if (!$this->enablePagination || $args === null) { + $contentItems = $this->contentLoader->find($query); + + return array_map( + function (int $contentId) use ($contentItems): Item { + return $this->itemFactory->fromContent( + $contentItems[array_search($contentId, array_column($contentItems, 'id'), true)] + ); + }, + $destinationContentIds + ); + } + + $paginator = new Paginator(function ($offset, $limit) use ($query): array { + $query->offset = $offset; + $query->limit = $limit ?? self::DEFAULT_LIMIT; + $contentItems = $this->contentLoader->find($query); + + return array_map( + function (Content $content): Item { + return $this->itemFactory->fromContent( + $content + ); + }, + $contentItems + ); + }); + + return PageAwareConnection::fromConnection( + $paginator->auto( + $args, + function () use ($query): int { + return $this->contentLoader->count($query); + } + ), + $args ); } + $query->limit = 1; + $contentItems = $this->contentLoader->find($query); + return $contentItems[0] ? $this->itemFactory->fromContent($contentItems[0]) : null; } @@ -62,11 +105,13 @@ private function getContentIds(Field $field): array { if ($field->value instanceof FieldType\RelationList\Value) { return $field->value->destinationContentIds; - } elseif ($field->value instanceof FieldType\Relation\Value) { + } + + if ($field->value instanceof FieldType\Relation\Value) { return [$field->value->destinationContentId]; - } else { - throw new UserError('\$field does not contain a RelationList or Relation Field value'); } + + throw new UserError('\$field does not contain a RelationList or Relation Field value'); } } diff --git a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php index dc6a4471..010e9b1b 100644 --- a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php +++ b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php @@ -14,24 +14,22 @@ class RelationFieldDefinitionMapper extends DecoratingFieldDefinitionMapper implements FieldDefinitionMapper { - /** - * @var \Ibexa\GraphQL\Schema\Domain\Content\NameHelper - */ - private $nameHelper; + private NameHelper $nameHelper; - /** - * @var \Ibexa\Contracts\Core\Repository\ContentTypeService - */ - private $contentTypeService; + private ContentTypeService $contentTypeService; + + private bool $enablePagination; public function __construct( FieldDefinitionMapper $innerMapper, NameHelper $nameHelper, - ContentTypeService $contentTypeService + ContentTypeService $contentTypeService, + bool $enablePagination ) { parent::__construct($innerMapper); $this->nameHelper = $nameHelper; $this->contentTypeService = $contentTypeService; + $this->enablePagination = $enablePagination; } public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string @@ -54,7 +52,18 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string } if ($this->isMultiple($fieldDefinition)) { - $type = "[$type]"; + if ($this->enablePagination) { + $type = 'RelationsConnection'; + } else { + @trigger_error( + 'Disable pagination for ezobjectrelationlist has been deprecated since version 4.6 ' . + 'and will be removed in version 5.0. To start receiving `RelationsConnection` instead of the deprecated ' . + '`[' . $type . ']`, set the parameter `ibexa.graphql.schema.ibexa_object_relation_list.enable_pagination` to `true`.', + E_USER_DEPRECATED + ); + + $type = "[$type]"; + } } return $type; @@ -68,7 +77,7 @@ public function mapToFieldValueResolver(FieldDefinition $fieldDefinition): ?stri $isMultiple = $this->isMultiple($fieldDefinition) ? 'true' : 'false'; - return sprintf('@=resolver("RelationFieldValue", [field, %s])', $isMultiple); + return sprintf('@=resolver("RelationFieldValue", [field, %s, args])', $isMultiple); } protected function canMap(FieldDefinition $fieldDefinition) @@ -76,6 +85,19 @@ protected function canMap(FieldDefinition $fieldDefinition) return in_array($fieldDefinition->fieldTypeIdentifier, ['ezobjectrelation', 'ezobjectrelationlist']); } + public function mapToFieldValueArgsBuilder(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + + if ($this->isMultiple($fieldDefinition) && $this->enablePagination) { + return 'Relay::Connection'; + } + + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + /** * Not implemented since we don't use it (canMap is overridden). */