diff --git a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php index ccd3e1b..95da72d 100644 --- a/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php +++ b/spec/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapperSpec.php @@ -61,19 +61,19 @@ public function it_maps_single_selection_with_a_unique_type_limitations_to_a_sin public function it_maps_multi_selection_without_type_limitations_to_an_array_of_generic_content(): void { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, []); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } public function it_maps_multi_selection_with_multiple_type_limitations_to_an_array_of_generic_content(): void { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_NONE, ['article', 'blog_post']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[Item]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } public function it_maps_multi_selection_with_a_unique_type_limitations_to_an_array_of_that_type(): void { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI, ['article']); - $this->mapToFieldValueType($fieldDefinition)->shouldReturn('[ArticleItem]'); + $this->mapToFieldValueType($fieldDefinition)->shouldReturn('RelationsConnection'); } public function it_delegates_the_field_definition_type_to_the_inner_mapper(FieldDefinitionMapper $innerMapper): void @@ -86,13 +86,13 @@ public function it_delegates_the_field_definition_type_to_the_inner_mapper(Field public function it_maps_multi_selection_to_resolve_multiple(): void { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_MULTI); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=query("RelationFieldValue", field, true)'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=query("RelationFieldValue", field, true, args)'); } public function it_maps_single_selection_to_resolve_single(): void { $fieldDefinition = $this->createFieldDefinition(self::DEF_LIMIT_SINGLE); - $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=query("RelationFieldValue", field, false)'); + $this->mapToFieldValueResolver($fieldDefinition)->shouldReturn('@=query("RelationFieldValue", field, false, args)'); } private function createFieldDefinition(int $selectionLimit = 0, array $selectionContentTypes = []): FieldDefinition diff --git a/src/bundle/Resources/config/graphql/Field.types.yaml b/src/bundle/Resources/config/graphql/Field.types.yaml index 17440b9..de60400 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/lib/Resolver/RelationFieldResolver.php b/src/lib/Resolver/RelationFieldResolver.php index f710a18..b3705db 100644 --- a/src/lib/Resolver/RelationFieldResolver.php +++ b/src/lib/Resolver/RelationFieldResolver.php @@ -8,15 +8,22 @@ 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\Definition\Resolver\QueryInterface; +use Overblog\GraphQLBundle\Relay\Connection\Paginator; final class RelationFieldResolver implements QueryInterface { + public const int DEFAULT_LIMIT = 25; + private ContentLoader $contentLoader; private ItemFactory $itemFactory; @@ -27,7 +34,7 @@ public function __construct(ContentLoader $contentLoader, ItemFactory $relatedCo $this->itemFactory = $relatedContentItemFactory; } - public function resolveRelationFieldValue(Field $field, $multiple = false) + public function resolveRelationFieldValue(Field $field, $multiple = false, ?Argument $args = null) { $destinationContentIds = $this->getContentIds($field); @@ -35,21 +42,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 ($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,10 +101,12 @@ 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 0af9b41..420c96a 100644 --- a/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php +++ b/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php @@ -49,7 +49,7 @@ public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string } if ($this->isMultiple($fieldDefinition)) { - $type = "[$type]"; + $type = 'RelationsConnection'; } return $type; @@ -63,7 +63,7 @@ public function mapToFieldValueResolver(FieldDefinition $fieldDefinition): ?stri $isMultiple = $this->isMultiple($fieldDefinition) ? 'true' : 'false'; - return sprintf('@=query("RelationFieldValue", field, %s)', $isMultiple); + return sprintf('@=query("RelationFieldValue", field, %s, args)', $isMultiple); } protected function canMap(FieldDefinition $fieldDefinition): bool @@ -71,6 +71,19 @@ protected function canMap(FieldDefinition $fieldDefinition): bool return in_array($fieldDefinition->fieldTypeIdentifier, ['ibexa_object_relation', 'ibexa_object_relation_list']); } + public function mapToFieldValueArgsBuilder(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + + if ($this->isMultiple($fieldDefinition)) { + return 'Relay::Connection'; + } + + return parent::mapToFieldValueArgsBuilder($fieldDefinition); + } + /** * Not implemented since we don't use it (canMap is overridden). */