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
46 changes: 46 additions & 0 deletions features/graphql/collection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -910,3 +910,49 @@ Feature: GraphQL collection support
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

@createSchema
Scenario: Retrieve paginated collections using mixed pagination
Given there are 5 fooDummy objects with fake names
When I send the following GraphQL request:
"""
{
fooDummies(page: 1) {
collection {
id
name
soManies(first: 2) {
edges {
node {
content
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
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[2].id" should exist
And the JSON node "data.fooDummies.collection[2].name" should exist
And the JSON node "data.fooDummies.collection[2].soManies" should exist
And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements
And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1"
And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA=="
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
36 changes: 31 additions & 5 deletions features/main/default_order.feature
Original file line number Diff line number Diff line change
Expand Up @@ -79,35 +79,61 @@ Feature: Default order
"@type": "FooDummy",
"id": 5,
"name": "Balbo",
"dummy": "/dummies/5"
"dummy": "/dummies/5",
"soManies": [
"/so_manies/13",
"/so_manies/14",
"/so_manies/15"
]

},
{
"@id": "/foo_dummies/3",
"@type": "FooDummy",
"id": 3,
"name": "Sthenelus",
"dummy": "/dummies/3"
"dummy": "/dummies/3",
"soManies": [
"/so_manies/7",
"/so_manies/8",
"/so_manies/9"
]
},
{
"@id": "/foo_dummies/2",
"@type": "FooDummy",
"id": 2,
"name": "Ephesian",
"dummy": "/dummies/2"
"dummy": "/dummies/2",
"soManies": [
"/so_manies/4",
"/so_manies/5",
"/so_manies/6"
]
},
{
"@id": "/foo_dummies/1",
"@type": "FooDummy",
"id": 1,
"name": "Hawsepipe",
"dummy": "/dummies/1"
"dummy": "/dummies/1",
"soManies": [
"/so_manies/1",
"/so_manies/2",
"/so_manies/3"
]
},
{
"@id": "/foo_dummies/4",
"@type": "FooDummy",
"id": 4,
"name": "Separativeness",
"dummy": "/dummies/4"
"dummy": "/dummies/4",
"soManies": [
"/so_manies/10",
"/so_manies/11",
"/so_manies/12"
]
}
],
"hydra:totalItems": 5,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\GraphQl\Metadata\Factory;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

final class GraphQlNestedOperationResourceMetadataFactory implements ResourceMetadataCollectionFactoryInterface
{
use OperationDefaultsTrait;

public function __construct(array $defaults, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null)
{
$this->defaults = $defaults;
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
$this->logger = $logger ?? new NullLogger();
}

public function create(string $resourceClass): ResourceMetadataCollection
{
$resourceMetadataCollection = new ResourceMetadataCollection($resourceClass);

if ($this->decorated) {
$resourceMetadataCollection = $this->decorated->create($resourceClass);
}

if (0 < \count($resourceMetadataCollection)) {
return $resourceMetadataCollection;
}

$shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;

$apiResource = new ApiResource(
class: $resourceClass,
shortName: $shortName
);

if (class_exists($resourceClass)) {
$refl = new \ReflectionClass($resourceClass);
$attribute = $refl->getAttributes(ApiResource::class)[0] ?? null;
$attributeInstance = $attribute?->newInstance();
if ($filters = $attributeInstance?->getFilters()) {
$apiResource = $apiResource->withFilters($filters);
}
}
Comment on lines +54 to +61

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: This should work with XML/YAML too.


$resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource);

return $resourceMetadataCollection;
}
}
71 changes: 32 additions & 39 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\Operation as AbstractOperation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand All @@ -47,7 +45,7 @@
*/
final class FieldsBuilder implements FieldsBuilderInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator)
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null)
{
}

Expand Down Expand Up @@ -256,7 +254,23 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
$resourceClass = $type->getClassName();
}

$graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);
$resourceOperation = $rootOperation;
if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
try {
$resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
} catch (OperationNotFoundException) {
// If there is no query operation for a nested resource we force one to exist
$nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass);
$resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
}
}

if (!$resourceOperation instanceof Operation) {
throw new \LogicException('The resource operation should be a GraphQL operation.');
}

$graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);

$graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType;
$isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
Expand All @@ -271,43 +285,22 @@ private function getResourceFieldConfiguration(?string $property, ?string $field

$args = [];

$resolverOperation = $rootOperation;

if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
$resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType);

if (!$resolverOperation instanceof Operation) {
$resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation);
}
}

if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
if ($this->pagination->isGraphQlEnabled($rootOperation)) {
$args = $this->getGraphQlPaginationArgs($rootOperation);
}

// Find the collection operation to get filters, there might be a smarter way to do this
$operation = null;
if (!empty($resourceClass)) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
try {
$operation = $resourceMetadataCollection->getOperation(null, true);
} catch (OperationNotFoundException) {
}
if ($this->pagination->isGraphQlEnabled($resourceOperation)) {
$args = $this->getGraphQlPaginationArgs($resourceOperation);
}

$args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation);
$args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
}

if ($isStandardGraphqlType || $input) {
$resolve = null;
} elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) {
$resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation);
$resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
} elseif ($this->typeBuilder->isCollection($type)) {
$resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation);
$resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
} else {
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation);
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
}

return [
Expand Down Expand Up @@ -368,21 +361,21 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array
return $args;
}

private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array
private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
{
if (null === $operation || null === $resourceClass) {
if (null === $resourceClass) {
return $args;
}

foreach ($operation->getFilters() ?? [] as $filterId) {
foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
if (!$this->filterLocator->has($filterId)) {
continue;
}

foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
$nullable = isset($value['required']) ? !$value['required'] : true;
$filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
$graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth);
$graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);

if (str_ends_with($key, '[]')) {
$graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
Expand All @@ -399,14 +392,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root
array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void {
$value = $graphqlFilterType;
});
$args = $this->mergeFilterArgs($args, $parsed, $operation, $key);
$args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
}
}

return $this->convertFilterArgsToTypes($args);
}

private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array
private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
{
foreach ($parsed as $key => $value) {
// Never override keys that cannot be merged
Expand Down Expand Up @@ -470,7 +463,7 @@ private function convertFilterArgsToTypes(array $args): array
*
* @throws InvalidTypeException
*/
private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull
private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull
{
$graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);

Expand All @@ -487,7 +480,7 @@ private function convertType(Type $type, bool $input, Operation $rootOperation,
}

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

return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
Expand Down
Loading