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 @@ -6,6 +6,7 @@
* GraphQL: Add support for custom queries and mutations
* GraphQL: Add support for custom types
* GraphQL: Better pagination support (backwards pagination)
* GraphQL: Support the pagination per resource
* GraphQL: Add the concept of *stages* in the workflow of the resolvers and add the possibility to disable them with operation attributes
* GraphQL: Add GraphQL Playground besides GraphiQL and add the possibility to change the default IDE (or to disable it) for the GraphQL endpoint
* GraphQL: Add a command to print the schema in SDL
Expand Down
20 changes: 20 additions & 0 deletions features/graphql/collection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,26 @@ Feature: GraphQL collection support
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.dummies.edges" should have 0 element

@createSchema
Scenario: Retrieve a collection with pagination disabled
Given there are 4 foo objects with fake names
When I send the following GraphQL request:
"""
{
foos {
id
name
bar
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.foos[3].id" should be equal to "/foos/4"
And the JSON node "data.foos[3].name" should be equal to "Separativeness"
And the JSON node "data.foos[3].bar" should be equal to "Sit"

Scenario: Custom collection query
Given there are 2 dummyCustomQuery objects
When I send the following GraphQL request:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC
return;
}

if (($context['graphql_operation_name'] ?? false) && !$this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context)) {
return;
}

$context = $this->addCountToContext(clone $aggregationBuilder, $context);

[, $offset, $limit] = $this->pagination->getPagination($resourceClass, $operationName, $context);
Expand Down Expand Up @@ -90,6 +94,10 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC
*/
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
{
if ($context['graphql_operation_name'] ?? false) {
return $this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context);
}

return $this->pagination->isEnabled($resourceClass, $operationName, $context);
}

Expand Down
8 changes: 8 additions & 0 deletions src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
*/
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
{
if ($context['graphql_operation_name'] ?? false) {
return $this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context);
}

if (null === $this->requestStack) {
return $this->pagination->isEnabled($resourceClass, $operationName, $context);
}
Expand Down Expand Up @@ -193,6 +197,10 @@ private function getPagination(QueryBuilder $queryBuilder, string $resourceClass
return null;
}

if (($context['graphql_operation_name'] ?? false) && !$this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context)) {
return null;
}

$context = $this->addCountToContext($queryBuilder, $context);

return \array_slice($this->pagination->getPagination($resourceClass, $operationName, $context), 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array
}

$container->setParameter('api_platform.graphql.default_ide', $config['graphql']['default_ide']);
$container->setParameter('api_platform.graphql.collection.pagination', $config['graphql']['collection']['pagination']);

$loader->load('graphql.xml');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ private function addGraphQlSection(ArrayNodeDefinition $rootNode): void
->arrayNode('graphql_playground')
->{class_exists(GraphQL::class) && class_exists(TwigBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->end()
->arrayNode('collection')
->addDefaultsIfNotSet()
->children()
->arrayNode('pagination')
->canBeDisabled()
->end()
->end()
->end()
->end()
->end()
->end();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<service id="api_platform.pagination" class="ApiPlatform\Core\DataProvider\Pagination">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument>%api_platform.collection.pagination%</argument>
<argument>%api_platform.graphql.collection.pagination%</argument>
</service>
</services>

Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
<argument>%api_platform.collection.pagination.enabled%</argument>
<argument type="service" id="api_platform.pagination" />
</service>

<service id="api_platform.graphql.resolver.stage.deserialize" class="ApiPlatform\Core\GraphQl\Resolver\Stage\DeserializeStage" public="false">
Expand Down Expand Up @@ -135,7 +135,7 @@
<argument type="service" id="api_platform.graphql.resolver.factory.collection" />
<argument type="service" id="api_platform.graphql.resolver.factory.item_mutation" />
<argument type="service" id="api_platform.filter_locator" />
<argument>%api_platform.collection.pagination.enabled%</argument>
<argument type="service" id="api_platform.pagination" />
</service>

<service id="api_platform.graphql.fields_builder_locator" class="Symfony\Component\DependencyInjection\ServiceLocator" public="false">
Expand Down
32 changes: 31 additions & 1 deletion src/DataProvider/Pagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\DataProvider;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;

/**
Expand All @@ -24,9 +25,10 @@
final class Pagination
{
private $options;
private $graphQlOptions;
private $resourceMetadataFactory;

public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $options = [])
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $options = [], array $graphQlOptions = [])
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->options = array_merge([
Expand All @@ -43,6 +45,9 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
'client_partial' => false,
'partial_parameter_name' => 'partial',
], $options);
$this->graphQlOptions = array_merge([
'enabled' => true,
], $graphQlOptions);
}

/**
Expand Down Expand Up @@ -171,6 +176,14 @@ public function isEnabled(string $resourceClass = null, string $operationName =
return $this->getEnabled($context, $resourceClass, $operationName);
}

/**
* Is the pagination enabled for GraphQL?
*/
public function isGraphQlEnabled(?string $resourceClass = null, ?string $operationName = null, array $context = []): bool
{
return $this->getGraphQlEnabled($resourceClass, $operationName);
}

/**
* Is the partial pagination enabled?
*/
Expand Down Expand Up @@ -198,6 +211,23 @@ private function getEnabled(array $context, string $resourceClass = null, string
return filter_var($this->getParameterFromContext($context, $this->options[$partial ? 'partial_parameter_name' : 'enabled_parameter_name'], $enabled), FILTER_VALIDATE_BOOLEAN);
}

return (bool) $enabled;
}

private function getGraphQlEnabled(?string $resourceClass, ?string $operationName): bool
{
$enabled = $this->graphQlOptions['enabled'];

if (null !== $resourceClass) {
try {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
} catch (ResourceClassNotFoundException $e) {
return $enabled;
}

return (bool) $resourceMetadata->getGraphqlAttribute($operationName, 'pagination_enabled', $enabled, true);
}

return $enabled;
}

Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Resolver/Factory/ItemResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
if (null !== $queryResolverId) {
/** @var QueryItemResolverInterface $queryResolver */
$queryResolver = $this->queryResolverLocator->get($queryResolverId);
$item = $queryResolver($item, ['source' => $source, 'args' => $args, 'info' => $info]);
$item = $queryResolver($item, $resolverContext);
$resourceClass = $this->getResourceClass($item, $resourceClass, $info, sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.');
}

Expand Down
8 changes: 4 additions & 4 deletions src/GraphQl/Resolver/Stage/ReadStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ public function __invoke(?string $resourceClass, ?string $rootClass, string $ope
$source = $context['source'];
if (isset($source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY])) {
$rootResolvedFields = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
$subresourceCollection = $this->getSubresource($rootClass, $rootResolvedFields, $rootProperty, $resourceClass, $normalizationContext);
$subresourceCollection = $this->getSubresource($rootClass, $rootResolvedFields, $rootProperty, $resourceClass, $normalizationContext, $operationName);
if (!is_iterable($subresourceCollection)) {
throw new \UnexpectedValueException('Expected subresource collection to be iterable');
}

return $subresourceCollection;
}

return $this->collectionDataProvider->getCollection($resourceClass, null, $normalizationContext);
return $this->collectionDataProvider->getCollection($resourceClass, $operationName, $normalizationContext);
}

private function getIdentifier(array $context): ?string
Expand Down Expand Up @@ -156,7 +156,7 @@ private function getNormalizedFilters(array $args): array
/**
* @return iterable|object|null
*/
private function getSubresource(string $rootClass, array $rootResolvedFields, string $rootProperty, string $subresourceClass, array $normalizationContext)
private function getSubresource(string $rootClass, array $rootResolvedFields, string $rootProperty, string $subresourceClass, array $normalizationContext, string $operationName)
{
$resolvedIdentifiers = [];
$rootIdentifiers = array_keys($rootResolvedFields);
Expand All @@ -168,6 +168,6 @@ private function getSubresource(string $rootClass, array $rootResolvedFields, st
'property' => $rootProperty,
'identifiers' => $resolvedIdentifiers,
'collection' => true,
]);
], $operationName);
}
}
11 changes: 6 additions & 5 deletions src/GraphQl/Resolver/Stage/SerializeStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\GraphQl\Resolver\Stage;

use ApiPlatform\Core\DataProvider\Pagination;
use ApiPlatform\Core\DataProvider\PaginatorInterface;
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\Core\GraphQl\Serializer\SerializerContextBuilderInterface;
Expand All @@ -33,14 +34,14 @@ final class SerializeStage implements SerializeStageInterface
private $resourceMetadataFactory;
private $normalizer;
private $serializerContextBuilder;
private $paginationEnabled;
private $pagination;

public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, SerializerContextBuilderInterface $serializerContextBuilder, bool $paginationEnabled)
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, SerializerContextBuilderInterface $serializerContextBuilder, Pagination $pagination)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->normalizer = $normalizer;
$this->serializerContextBuilder = $serializerContextBuilder;
$this->paginationEnabled = $paginationEnabled;
$this->pagination = $pagination;
}

/**
Expand All @@ -54,7 +55,7 @@ public function __invoke($itemOrCollection, string $resourceClass, string $opera
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
if (!$resourceMetadata->getGraphqlAttribute($operationName, 'serialize', true, true)) {
if ($isCollection) {
if ($this->paginationEnabled) {
if ($this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context)) {
return $this->getDefaultPaginatedData();
}

Expand Down Expand Up @@ -84,7 +85,7 @@ public function __invoke($itemOrCollection, string $resourceClass, string $opera
}

if ($isCollection && is_iterable($itemOrCollection)) {
if (!$this->paginationEnabled) {
if (!$this->pagination->isGraphQlEnabled($resourceClass, $operationName, $context)) {
$data = [];
foreach ($itemOrCollection as $index => $object) {
$data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
Expand Down
11 changes: 6 additions & 5 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\GraphQl\Type;

use ApiPlatform\Core\DataProvider\Pagination;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
Expand Down Expand Up @@ -48,9 +49,9 @@ final class FieldsBuilder implements FieldsBuilderInterface
private $collectionResolverFactory;
private $itemMutationResolverFactory;
private $filterLocator;
private $paginationEnabled;
private $pagination;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, TypesContainerInterface $typesContainer, TypeBuilderInterface $typeBuilder, TypeConverterInterface $typeConverter, ResolverFactoryInterface $itemResolverFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, ContainerInterface $filterLocator, bool $paginationEnabled)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, TypesContainerInterface $typesContainer, TypeBuilderInterface $typeBuilder, TypeConverterInterface $typeConverter, ResolverFactoryInterface $itemResolverFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, ContainerInterface $filterLocator, Pagination $pagination)
{
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
Expand All @@ -62,7 +63,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->collectionResolverFactory = $collectionResolverFactory;
$this->itemMutationResolverFactory = $itemMutationResolverFactory;
$this->filterLocator = $filterLocator;
$this->paginationEnabled = $paginationEnabled;
$this->pagination = $pagination;
}

/**
Expand Down Expand Up @@ -247,7 +248,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field

$args = [];
if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) {
if ($this->paginationEnabled) {
if ($this->pagination->isGraphQlEnabled($resourceClass, $queryName)) {
$args = [
'first' => [
'type' => GraphQLType::int(),
Expand Down Expand Up @@ -409,7 +410,7 @@ private function convertType(Type $type, bool $input, ?string $queryName, ?strin
}

if ($this->typeBuilder->isCollection($type)) {
return $this->paginationEnabled && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
return $this->pagination->isGraphQlEnabled($resourceClass, $queryName ?? $mutationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
}

return !$graphqlType instanceof NullableType || $type->isNullable() || (null !== $mutationName && 'update' === $mutationName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,28 @@ public function testApplyToCollectionPaginationDisabled()
$extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context);
}

public function testApplyToCollectionGraphQlPaginationDisabled()
{
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], []));
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();

$pagination = new Pagination($resourceMetadataFactory, [], [
'enabled' => false,
]);

$aggregationBuilderProphecy = $this->prophesize(Builder::class);
$aggregationBuilderProphecy->facet()->shouldNotBeCalled();

$context = ['graphql_operation_name' => 'op'];

$extension = new PaginationExtension(
$this->managerRegistryProphecy->reveal(),
$pagination
);
$extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context);
}

public function testApplyToCollectionWithMaximumItemsPerPage()
{
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
Expand Down Expand Up @@ -368,6 +390,23 @@ public function testSupportsResultPaginationDisabled()
$this->assertFalse($extension->supportsResult('Foo', 'op', ['filters' => ['enabled' => false]]));
}

public function testSupportsResultGraphQlPaginationDisabled()
{
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], []));
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();

$pagination = new Pagination($resourceMetadataFactory, [], [
'enabled' => false,
]);

$extension = new PaginationExtension(
$this->managerRegistryProphecy->reveal(),
$pagination
);
$this->assertFalse($extension->supportsResult('Foo', 'op', ['filters' => ['enabled' => false], 'graphql_operation_name' => 'op']));
}

public function testGetResult()
{
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
Expand Down
Loading