Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
99 changes: 99 additions & 0 deletions features/graphql/collection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
<argument type="service" id="api_platform.graphql.types_container" />
<argument type="service" id="api_platform.graphql.resolver.resource_field" />
<argument type="service" id="api_platform.graphql.fields_builder_locator" />
<argument type="service" id="api_platform.pagination" />
</service>

<service id="api_platform.graphql.fields_builder" class="ApiPlatform\Core\GraphQl\Type\FieldsBuilder" public="false">
Expand Down
12 changes: 12 additions & 0 deletions src/DataProvider/Pagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -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?
*/
Expand Down
40 changes: 35 additions & 5 deletions src/GraphQl/Resolver/Stage/SerializeStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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'];

Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand Down
67 changes: 48 additions & 19 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
Loading