Skip to content

Commit

Permalink
feat(graphql): support nullable embedded relations in GraphQL types (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenstell authored Feb 20, 2024
1 parent 6b00cea commit 89c9229
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 116 deletions.
13 changes: 13 additions & 0 deletions features/graphql/schema.feature
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,16 @@ Feature: GraphQL schema-related features
clientMutationId: String
}
"""
And the command output should contain:
"""
"Updates a OptionalRequiredDummy."
input updateOptionalRequiredDummyInput {
id: ID!
thirdLevel: updateThirdLevelNestedInput
thirdLevelRequired: updateThirdLevelNestedInput!
"Get relatedToDummyFriend."
relatedToDummyFriend: [updateRelatedToDummyFriendNestedInput]
clientMutationId: String
}
"""
4 changes: 2 additions & 2 deletions src/GraphQl/Tests/Type/FieldsBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
use ApiPlatform\GraphQl\Tests\Fixtures\Serializer\NameConverter\CustomConverter;
use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface;
use ApiPlatform\GraphQl\Type\FieldsBuilder;
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
use ApiPlatform\GraphQl\Type\TypeConverterInterface;
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
use ApiPlatform\Metadata\ApiProperty;
Expand Down Expand Up @@ -79,7 +79,7 @@ protected function setUp(): void
$this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class);
$this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class);
$this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class);
$this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class);
$this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class);
$this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class);
Expand Down
89 changes: 60 additions & 29 deletions src/GraphQl/Tests/Type/TypeBuilderTest.php

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions src/GraphQl/Tests/Type/TypeConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
use ApiPlatform\GraphQl\Tests\Fixtures\Type\Definition\DateTimeType;
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface;
use ApiPlatform\GraphQl\Type\TypeConverter;
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
use ApiPlatform\Metadata\ApiProperty;
Expand Down Expand Up @@ -54,7 +54,7 @@ class TypeConverterTest extends TestCase
*/
protected function setUp(): void
{
$this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class);
$this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class);
$this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class);
$this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
Expand Down Expand Up @@ -155,13 +155,15 @@ public function testConvertTypeInputResource(): void
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy');
/** @var Operation $operation */
$operation = new Query();
/** @var ApiProperty $propertyMetadata */
$propertyMetadata = (new ApiProperty())->withWritableLink(true);
$graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['item_query' => $operation])]);
$expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType', 'fields' => []]);

$this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata);
$this->typeBuilderProphecy->isCollection($type)->willReturn(false);
$this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withWritableLink(true));
$this->typeBuilderProphecy->getResourceObjectType('dummy', $graphqlResourceMetadata, $operation, true, false, 1)->shouldBeCalled()->willReturn($expectedGraphqlType);
$this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $operation, $propertyMetadata, ['input' => true, 'wrapped' => false, 'depth' => 1])->shouldBeCalled()->willReturn($expectedGraphqlType);

$graphqlType = $this->typeConverter->convertType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1);
$this->assertSame($expectedGraphqlType, $graphqlType);
Expand All @@ -179,7 +181,11 @@ public function testConvertTypeCollectionResource(Type $type, ObjectType $expect

$this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true);
$this->resourceMetadataCollectionFactoryProphecy->create('dummyValue')->shouldBeCalled()->willReturn($graphqlResourceMetadata);
$this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, $collectionOperation, false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType);
$this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $collectionOperation, null, [
'input' => false,
'wrapped' => false,
'depth' => 0,
])->shouldBeCalled()->willReturn($expectedGraphqlType);

/** @var Operation $rootOperation */
$rootOperation = (new Query())->withName('test');
Expand Down
58 changes: 58 additions & 0 deletions src/GraphQl/Type/ContextAwareTypeBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Type;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;

/**
* Interface implemented to build a GraphQL type.
*
* @author Antoine Bluchet <[email protected]>
*/
interface ContextAwareTypeBuilderInterface
{
/**
* Gets the object type of the given resource.
*
* @param array<string, mixed>&array{input?: bool, wrapped?: bool, depth?: int} $context
*
* @return GraphQLType the object type, possibly wrapped by NonNull
*/
public function getResourceObjectType(ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, ?ApiProperty $propertyMetadata = null, array $context = []): GraphQLType;

/**
* Get the interface type of a node.
*/
public function getNodeInterface(): InterfaceType;

/**
* Gets the type of a paginated collection of the given resource type.
*/
public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType;

/**
* Gets the type corresponding to an enum.
*/
public function getEnumType(Operation $operation): GraphQLType;

/**
* Returns true if a type is a collection.
*/
public function isCollection(Type $type): bool;
}
7 changes: 5 additions & 2 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@
*/
final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface
{
private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;
private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;

public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, TypeBuilderEnumInterface|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, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|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)
{
if ($typeBuilder instanceof TypeBuilderInterface) {
@trigger_error(sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
}
if ($typeBuilder instanceof TypeBuilderEnumInterface) {
@trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED);
}
$this->typeBuilder = $typeBuilder;
}

Expand Down
Loading

0 comments on commit 89c9229

Please sign in to comment.