diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63a47239a6a..fc25a0d1eeb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* MongoDB: Possibility to add execute options (aggregate command fields) for a resource, like `allowDiskUse` (#3144)
* GraphQL: Allow to format GraphQL errors based on exceptions (#3063)
+* GraphQL: Add page-based pagination (#3175)
## 2.5.2
diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature
index eb581455912..1080036b166 100644
--- a/features/graphql/collection.feature
+++ b/features/graphql/collection.feature
@@ -680,3 +680,102 @@ Feature: GraphQL collection support
And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[0].node.title" should not exist
And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[1].node.title" should not exist
And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[2].node.title" should not exist
+
+ @createSchema
+ Scenario: Retrieve a paginated collection using page-based pagination
+ Given there are 5 fooDummy objects with fake names
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 1) {
+ collection {
+ id
+ }
+ paginationInfo {
+ itemsPerPage
+ lastPage
+ totalCount
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 3 elements
+ And the JSON node "data.fooDummies.collection[0].id" should exist
+ And the JSON node "data.fooDummies.collection[1].id" should exist
+ And the JSON node "data.fooDummies.collection[2].id" should exist
+ And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
+ And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
+ And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 2) {
+ collection {
+ id
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 2 elements
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 3) {
+ collection {
+ id
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 0 elements
+
+ @createSchema
+ Scenario: Retrieve a paginated collection using page-based pagination and client-defined limit
+ Given there are 5 fooDummy objects with fake names
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 1, itemsPerPage: 2) {
+ collection {
+ id
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 2 elements
+ And the JSON node "data.fooDummies.collection[0].id" should exist
+ And the JSON node "data.fooDummies.collection[1].id" should exist
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 2, itemsPerPage: 2) {
+ collection {
+ id
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 2 elements
+ When I send the following GraphQL request:
+ """
+ {
+ fooDummies(page: 3, itemsPerPage: 2) {
+ collection {
+ id
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the JSON node "data.fooDummies.collection" should have 1 element
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
index 117b1ab8f87..ef5cc6d0590 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
@@ -127,6 +127,7 @@
+
diff --git a/src/DataProvider/Pagination.php b/src/DataProvider/Pagination.php
index 8a9bc41b9cd..b91ea79bd0b 100644
--- a/src/DataProvider/Pagination.php
+++ b/src/DataProvider/Pagination.php
@@ -196,6 +196,18 @@ public function isPartialEnabled(string $resourceClass = null, string $operation
return $this->getEnabled($context, $resourceClass, $operationName, true);
}
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function getGraphQlPaginationType(string $resourceClass, string $operationName): string
+ {
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+
+ return (string) $resourceMetadata->getGraphqlAttribute($operationName, 'paginationType', 'cursor', true);
+ }
+
/**
* Is the classic or partial pagination enabled?
*/
diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php
index 18e72b51a8e..92cc52a7022 100644
--- a/src/GraphQl/Resolver/Stage/SerializeStage.php
+++ b/src/GraphQl/Resolver/Stage/SerializeStage.php
@@ -54,7 +54,9 @@ public function __invoke($itemOrCollection, string $resourceClass, string $opera
if (!$resourceMetadata->getGraphqlAttribute($operationName, 'serialize', true, true)) {
if ($isCollection) {
if ($this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context)) {
- return $this->getDefaultPaginatedData();
+ return 'cursor' === $this->pagination->getGraphQlPaginationType($resourceClass, $operationName) ?
+ $this->getDefaultCursorBasedPaginatedData() :
+ $this->getDefaultPageBasedPaginatedData();
}
return [];
@@ -87,7 +89,9 @@ public function __invoke($itemOrCollection, string $resourceClass, string $opera
$data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
}
} else {
- $data = $this->serializePaginatedCollection($itemOrCollection, $normalizationContext, $context);
+ $data = 'cursor' === $this->pagination->getGraphQlPaginationType($resourceClass, $operationName) ?
+ $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) :
+ $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext);
}
}
@@ -108,7 +112,7 @@ public function __invoke($itemOrCollection, string $resourceClass, string $opera
* @throws \LogicException
* @throws \UnexpectedValueException
*/
- private function serializePaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
+ private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
{
$args = $context['args'];
@@ -138,7 +142,7 @@ private function serializePaginatedCollection(iterable $collection, array $norma
}
$offset = 0 > $offset ? 0 : $offset;
- $data = $this->getDefaultPaginatedData();
+ $data = $this->getDefaultCursorBasedPaginatedData();
if (($totalItems = $collection->getTotalItems()) > 0) {
$data['totalCount'] = $totalItems;
@@ -161,11 +165,37 @@ private function serializePaginatedCollection(iterable $collection, array $norma
return $data;
}
- private function getDefaultPaginatedData(): array
+ /**
+ * @throws \LogicException
+ */
+ private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext): array
+ {
+ if (!($collection instanceof PaginatorInterface)) {
+ throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s.', PaginatorInterface::class));
+ }
+
+ $data = $this->getDefaultPageBasedPaginatedData();
+ $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
+ $data['paginationInfo']['lastPage'] = $collection->getLastPage();
+ $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
+
+ foreach ($collection as $object) {
+ $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
+ }
+
+ return $data;
+ }
+
+ private function getDefaultCursorBasedPaginatedData(): array
{
return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
}
+ private function getDefaultPageBasedPaginatedData(): array
+ {
+ return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0.]];
+ }
+
private function getDefaultMutationData(array $context): array
{
return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php
index 64d0d47a75c..a18e24e654c 100644
--- a/src/GraphQl/Type/FieldsBuilder.php
+++ b/src/GraphQl/Type/FieldsBuilder.php
@@ -254,24 +254,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
$args = [];
if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) {
if ($this->pagination->isGraphQlEnabled($resourceClass, $queryName)) {
- $args = [
- 'first' => [
- 'type' => GraphQLType::int(),
- 'description' => 'Returns the first n elements from the list.',
- ],
- 'last' => [
- 'type' => GraphQLType::int(),
- 'description' => 'Returns the last n elements from the list.',
- ],
- 'before' => [
- 'type' => GraphQLType::string(),
- 'description' => 'Returns the elements in the list that come before the specified cursor.',
- ],
- 'after' => [
- 'type' => GraphQLType::string(),
- 'description' => 'Returns the elements in the list that come after the specified cursor.',
- ],
- ];
+ $args = $this->getGraphQlPaginationArgs($resourceClass, $queryName);
}
$args = $this->getFilterArgs($args, $resourceClass, $resourceMetadata, $rootResource, $property, $queryName, $mutationName, $depth);
@@ -299,6 +282,50 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
return null;
}
+ private function getGraphQlPaginationArgs(string $resourceClass, string $queryName): array
+ {
+ $paginationType = $this->pagination->getGraphQlPaginationType($resourceClass, $queryName);
+
+ if ('cursor' === $paginationType) {
+ return [
+ 'first' => [
+ 'type' => GraphQLType::int(),
+ 'description' => 'Returns the first n elements from the list.',
+ ],
+ 'last' => [
+ 'type' => GraphQLType::int(),
+ 'description' => 'Returns the last n elements from the list.',
+ ],
+ 'before' => [
+ 'type' => GraphQLType::string(),
+ 'description' => 'Returns the elements in the list that come before the specified cursor.',
+ ],
+ 'after' => [
+ 'type' => GraphQLType::string(),
+ 'description' => 'Returns the elements in the list that come after the specified cursor.',
+ ],
+ ];
+ }
+
+ $paginationOptions = $this->pagination->getOptions();
+
+ $args = [
+ $paginationOptions['page_parameter_name'] => [
+ 'type' => GraphQLType::int(),
+ 'description' => 'Returns the current page.',
+ ],
+ ];
+
+ if ($paginationOptions['client_items_per_page']) {
+ $args[$paginationOptions['items_per_page_parameter_name']] = [
+ 'type' => GraphQLType::int(),
+ 'description' => 'Returns the number of items per page.',
+ ];
+ }
+
+ return $args;
+ }
+
private function getFilterArgs(array $args, ?string $resourceClass, ?ResourceMetadata $resourceMetadata, string $rootResource, ?string $property, ?string $queryName, ?string $mutationName, int $depth): array
{
if (null === $resourceMetadata || null === $resourceClass) {
@@ -418,7 +445,9 @@ private function convertType(Type $type, bool $input, ?string $queryName, ?strin
}
if ($this->typeBuilder->isCollection($type)) {
- return $this->pagination->isGraphQlEnabled($resourceClass, $queryName ?? $mutationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
+ $operationName = $queryName ?? $mutationName;
+
+ return $this->pagination->isGraphQlEnabled($resourceClass, $operationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operationName) : GraphQLType::listOf($graphqlType);
}
return !$graphqlType instanceof NullableType || $type->isNullable() || (null !== $mutationName && 'update' === $mutationName)
diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php
index 66b6b23fcc3..c4c3d87d65c 100644
--- a/src/GraphQl/Type/TypeBuilder.php
+++ b/src/GraphQl/Type/TypeBuilder.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\GraphQl\Type;
+use ApiPlatform\Core\DataProvider\Pagination;
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use GraphQL\Type\Definition\InputObjectType;
@@ -35,12 +36,14 @@ final class TypeBuilder implements TypeBuilderInterface
private $typesContainer;
private $defaultFieldResolver;
private $fieldsBuilderLocator;
+ private $pagination;
- public function __construct(TypesContainerInterface $typesContainer, callable $defaultFieldResolver, ContainerInterface $fieldsBuilderLocator)
+ public function __construct(TypesContainerInterface $typesContainer, callable $defaultFieldResolver, ContainerInterface $fieldsBuilderLocator, Pagination $pagination)
{
$this->typesContainer = $typesContainer;
$this->defaultFieldResolver = $defaultFieldResolver;
$this->fieldsBuilderLocator = $fieldsBuilderLocator;
+ $this->pagination = $pagination;
}
/**
@@ -171,7 +174,7 @@ public function getNodeInterface(): InterfaceType
/**
* {@inheritdoc}
*/
- public function getResourcePaginatedCollectionType(GraphQLType $resourceType): GraphQLType
+ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, string $operationName): GraphQLType
{
$shortName = $resourceType->name;
@@ -179,6 +182,36 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType): G
return $this->typesContainer->get("{$shortName}Connection");
}
+ $paginationType = $this->pagination->getGraphQlPaginationType($resourceClass, $operationName);
+
+ $fields = 'cursor' === $paginationType ?
+ $this->getCursorBasedPaginationFields($resourceType) :
+ $this->getPageBasedPaginationFields($resourceType);
+
+ $configuration = [
+ 'name' => "{$shortName}Connection",
+ 'description' => "Connection for $shortName.",
+ 'fields' => $fields,
+ ];
+
+ $resourcePaginatedCollectionType = new ObjectType($configuration);
+ $this->typesContainer->set("{$shortName}Connection", $resourcePaginatedCollectionType);
+
+ return $resourcePaginatedCollectionType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isCollection(Type $type): bool
+ {
+ return $type->isCollection() && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType();
+ }
+
+ private function getCursorBasedPaginationFields(GraphQLType $resourceType): array
+ {
+ $shortName = $resourceType->name;
+
$edgeObjectTypeConfiguration = [
'name' => "{$shortName}Edge",
'description' => "Edge of $shortName.",
@@ -203,27 +236,32 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType): G
$pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration);
$this->typesContainer->set("{$shortName}PageInfo", $pageInfoObjectType);
- $configuration = [
- 'name' => "{$shortName}Connection",
- 'description' => "Connection for $shortName.",
+ return [
+ 'edges' => GraphQLType::listOf($edgeObjectType),
+ 'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
+ 'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
+ ];
+ }
+
+ private function getPageBasedPaginationFields(GraphQLType $resourceType): array
+ {
+ $shortName = $resourceType->name;
+
+ $paginationInfoObjectTypeConfiguration = [
+ 'name' => "{$shortName}PaginationInfo",
+ 'description' => 'Information about the pagination.',
'fields' => [
- 'edges' => GraphQLType::listOf($edgeObjectType),
- 'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
+ 'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
+ 'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
],
];
+ $paginationInfoObjectType = new ObjectType($paginationInfoObjectTypeConfiguration);
+ $this->typesContainer->set("{$shortName}PaginationInfo", $paginationInfoObjectType);
- $resourcePaginatedCollectionType = new ObjectType($configuration);
- $this->typesContainer->set("{$shortName}Connection", $resourcePaginatedCollectionType);
-
- return $resourcePaginatedCollectionType;
- }
-
- /**
- * {@inheritdoc}
- */
- public function isCollection(Type $type): bool
- {
- return $type->isCollection() && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType();
+ return [
+ 'collection' => GraphQLType::listOf($resourceType),
+ 'paginationInfo' => GraphQLType::nonNull($paginationInfoObjectType),
+ ];
}
}
diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php
index 138cf8bd3e0..b59c2093e5a 100644
--- a/src/GraphQl/Type/TypeBuilderInterface.php
+++ b/src/GraphQl/Type/TypeBuilderInterface.php
@@ -44,7 +44,7 @@ public function getNodeInterface(): InterfaceType;
/**
* Gets the type of a paginated collection of the given resource type.
*/
- public function getResourcePaginatedCollectionType(GraphQLType $resourceType): GraphQLType;
+ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, string $operationName): GraphQLType;
/**
* Returns true if a type is a collection.
diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php
index 2cb5ef90f57..90c9de2aec8 100644
--- a/tests/Fixtures/TestBundle/Document/FooDummy.php
+++ b/tests/Fixtures/TestBundle/Document/FooDummy.php
@@ -21,9 +21,14 @@
*
* @author Vincent Chalamon
*
- * @ApiResource(attributes={
- * "order"={"dummy.name"}
- * })
+ * @ApiResource(
+ * attributes={
+ * "order"={"dummy.name"}
+ * },
+ * graphql={
+ * "collection_query"={"paginationType"="page"}
+ * }
+ * )
* @ODM\Document
*/
class FooDummy
diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php
index a06658a5707..1ec0a15a8cb 100644
--- a/tests/Fixtures/TestBundle/Entity/FooDummy.php
+++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php
@@ -21,9 +21,14 @@
*
* @author Vincent Chalamon
*
- * @ApiResource(attributes={
- * "order"={"dummy.name"}
- * })
+ * @ApiResource(
+ * attributes={
+ * "order"={"dummy.name"}
+ * },
+ * graphql={
+ * "collection_query"={"paginationType"="page"}
+ * }
+ * )
* @ORM\Entity
*/
class FooDummy
diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php
index 014113d45ed..5ae8f81fd29 100644
--- a/tests/GraphQl/Type/FieldsBuilderTest.php
+++ b/tests/GraphQl/Type/FieldsBuilderTest.php
@@ -206,7 +206,7 @@ public function testGetCollectionQueryFields(string $resourceClass, ResourceMeta
$this->typeConverterProphecy->convertType(Argument::type(Type::class), false, $queryName, null, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType);
$this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string());
$this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true);
- $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType)->willReturn($graphqlType);
+ $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $queryName)->willReturn($graphqlType);
$this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata);
$this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $queryName)->willReturn($resolver);
$this->filterLocatorProphecy->has('my_filter')->willReturn(true);
@@ -325,6 +325,27 @@ public function collectionQueryFieldsProvider(): array
],
],
],
+ 'collection with page-based pagination enabled' => ['resourceClass', (new ResourceMetadata('ShortName', null, null, null, null, ['paginationType' => 'page']))->withGraphql(['action' => ['filters' => ['my_filter']]]), 'action', [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () {
+ },
+ [
+ 'actionShortNames' => [
+ 'type' => $graphqlType,
+ 'description' => null,
+ 'args' => [
+ 'page' => [
+ 'type' => GraphQLType::int(),
+ 'description' => 'Returns the current page.',
+ ],
+ 'boolField' => $graphqlType,
+ 'boolField_list' => GraphQLType::listOf($graphqlType),
+ 'parent__child' => new InputObjectType(['name' => 'ShortNameFilter_parent__child', 'fields' => ['related__nested' => $graphqlType]]),
+ 'dateField' => new InputObjectType(['name' => 'ShortNameFilter_dateField', 'fields' => ['before' => $graphqlType]]),
+ ],
+ 'resolve' => $resolver,
+ 'deprecationReason' => '',
+ ],
+ ],
+ ],
];
}
@@ -336,7 +357,7 @@ public function testGetMutationFields(string $resourceClass, ResourceMetadata $r
$this->typeConverterProphecy->convertType(Argument::type(Type::class), false, null, $mutationName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType);
$this->typeConverterProphecy->convertType(Argument::type(Type::class), true, null, $mutationName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType);
$this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn($isTypeCollection);
- $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType)->willReturn($graphqlType);
+ $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $mutationName)->willReturn($graphqlType);
$this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata());
$this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, null)->willReturn($collectionResolver);
$this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, null, $mutationName)->willReturn($mutationResolver);
diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php
index 557686991b8..4875ebf0905 100644
--- a/tests/GraphQl/Type/TypeBuilderTest.php
+++ b/tests/GraphQl/Type/TypeBuilderTest.php
@@ -13,10 +13,12 @@
namespace ApiPlatform\Core\Tests\GraphQl\Type;
+use ApiPlatform\Core\DataProvider\Pagination;
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\Core\GraphQl\Type\FieldsBuilderInterface;
use ApiPlatform\Core\GraphQl\Type\TypeBuilder;
use ApiPlatform\Core\GraphQl\Type\TypesContainerInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use GraphQL\Type\Definition\InputObjectType;
@@ -46,6 +48,9 @@ class TypeBuilderTest extends TestCase
/** @var ObjectProphecy */
private $fieldsBuilderLocatorProphecy;
+ /** @var ObjectProphecy */
+ private $resourceMetadataFactoryProphecy;
+
/** @var TypeBuilder */
private $typeBuilder;
@@ -58,7 +63,13 @@ protected function setUp(): void
$this->defaultFieldResolver = function () {
};
$this->fieldsBuilderLocatorProphecy = $this->prophesize(ContainerInterface::class);
- $this->typeBuilder = new TypeBuilder($this->typesContainerProphecy->reveal(), $this->defaultFieldResolver, $this->fieldsBuilderLocatorProphecy->reveal());
+ $this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $this->typeBuilder = new TypeBuilder(
+ $this->typesContainerProphecy->reveal(),
+ $this->defaultFieldResolver,
+ $this->fieldsBuilderLocatorProphecy->reveal(),
+ new Pagination($this->resourceMetadataFactoryProphecy->reveal())
+ );
}
public function testGetResourceObjectType(): void
@@ -316,15 +327,24 @@ public function testGetNodeInterface(): void
$this->assertSame(GraphQLType::string(), $resolvedType);
}
- public function testGetResourcePaginatedCollectionType(): void
+ public function testCursorBasedGetResourcePaginatedCollectionType(): void
{
$this->typesContainerProphecy->has('StringConnection')->shouldBeCalled()->willReturn(false);
$this->typesContainerProphecy->set('StringConnection', Argument::type(ObjectType::class))->shouldBeCalled();
$this->typesContainerProphecy->set('StringEdge', Argument::type(ObjectType::class))->shouldBeCalled();
$this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled();
+ $this->resourceMetadataFactoryProphecy->create('StringResourceClass')->shouldBeCalled()->willReturn(new ResourceMetadata(
+ null,
+ null,
+ null,
+ null,
+ null,
+ ['paginationType' => 'cursor']
+ ));
+
/** @var ObjectType $resourcePaginatedCollectionType */
- $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string());
+ $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', 'operationName');
$this->assertSame('StringConnection', $resourcePaginatedCollectionType->name);
$this->assertSame('Connection for String.', $resourcePaginatedCollectionType->description);
@@ -370,6 +390,48 @@ public function testGetResourcePaginatedCollectionType(): void
$this->assertSame(GraphQLType::int(), $totalCountType->getWrappedType());
}
+ public function testPageBasedGetResourcePaginatedCollectionType(): void
+ {
+ $this->typesContainerProphecy->has('StringConnection')->shouldBeCalled()->willReturn(false);
+ $this->typesContainerProphecy->set('StringConnection', Argument::type(ObjectType::class))->shouldBeCalled();
+ $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled();
+
+ $this->resourceMetadataFactoryProphecy->create('StringResourceClass')->shouldBeCalled()->willReturn(new ResourceMetadata(
+ null,
+ null,
+ null,
+ null,
+ null,
+ ['paginationType' => 'page']
+ ));
+
+ /** @var ObjectType $resourcePaginatedCollectionType */
+ $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', 'operationName');
+ $this->assertSame('StringConnection', $resourcePaginatedCollectionType->name);
+ $this->assertSame('Connection for String.', $resourcePaginatedCollectionType->description);
+
+ $resourcePaginatedCollectionTypeFields = $resourcePaginatedCollectionType->getFields();
+ $this->assertArrayHasKey('collection', $resourcePaginatedCollectionTypeFields);
+ $this->assertArrayHasKey('paginationInfo', $resourcePaginatedCollectionTypeFields);
+
+ /** @var NonNull $paginationInfoType */
+ $paginationInfoType = $resourcePaginatedCollectionTypeFields['paginationInfo']->getType();
+ /** @var ObjectType $wrappedType */
+ $wrappedType = $paginationInfoType->getWrappedType();
+ $this->assertSame('StringPaginationInfo', $wrappedType->name);
+ $this->assertSame('Information about the pagination.', $wrappedType->description);
+ $paginationInfoObjectTypeFields = $wrappedType->getFields();
+ $this->assertArrayHasKey('itemsPerPage', $paginationInfoObjectTypeFields);
+ $this->assertArrayHasKey('lastPage', $paginationInfoObjectTypeFields);
+ $this->assertArrayHasKey('totalCount', $paginationInfoObjectTypeFields);
+ $this->assertInstanceOf(NonNull::class, $paginationInfoObjectTypeFields['itemsPerPage']->getType());
+ $this->assertSame(GraphQLType::int(), $paginationInfoObjectTypeFields['itemsPerPage']->getType()->getWrappedType());
+ $this->assertInstanceOf(NonNull::class, $paginationInfoObjectTypeFields['lastPage']->getType());
+ $this->assertSame(GraphQLType::int(), $paginationInfoObjectTypeFields['lastPage']->getType()->getWrappedType());
+ $this->assertInstanceOf(NonNull::class, $paginationInfoObjectTypeFields['totalCount']->getType());
+ $this->assertSame(GraphQLType::int(), $paginationInfoObjectTypeFields['totalCount']->getType()->getWrappedType());
+ }
+
/**
* @dataProvider typesProvider
*/