From 384b648aef7ea881ca3e924eebe1ef0d70f983e6 Mon Sep 17 00:00:00 2001 From: Koen Pasman Date: Wed, 10 Jan 2024 11:54:56 +0100 Subject: [PATCH] fix(graphql): support nullable embedded relations in GraphQL types --- src/GraphQl/Tests/Type/TypeBuilderTest.php | 24 +++++++++++++++++++ src/GraphQl/Tests/Type/TypeConverterTest.php | 4 ++-- src/GraphQl/Type/TypeBuilder.php | 16 +++++++++---- src/GraphQl/Type/TypeBuilderEnumInterface.php | 6 ++--- src/GraphQl/Type/TypeConverter.php | 4 +++- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/GraphQl/Tests/Type/TypeBuilderTest.php b/src/GraphQl/Tests/Type/TypeBuilderTest.php index c9afecb7af4..f551c5d369b 100644 --- a/src/GraphQl/Tests/Type/TypeBuilderTest.php +++ b/src/GraphQl/Tests/Type/TypeBuilderTest.php @@ -217,6 +217,30 @@ public function testGetResourceObjectTypeNestedInput(): void $wrappedType->config['fields'](); } + public function testGetResourceObjectTypeNestedInputNullable(): void + { + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $this->typesContainerProphecy->has('customShortNameNullableNestedInput')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('customShortNameNullableNestedInput', Argument::type(InputObjectType::class))->shouldBeCalled(); + $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + + /** @var Operation $operation */ + $operation = (new Mutation())->withName('custom')->withShortName('shortNameNullable')->withDescription('description nullable'); + /** @var InputObjectType $resourceObjectType */ + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, true, false, 1, false); + + $this->assertInstanceOf(InputObjectType::class, $resourceObjectType); + $this->assertSame('customShortNameNullableNestedInput', $resourceObjectType->name); + $this->assertSame('description nullable', $resourceObjectType->description); + $this->assertArrayHasKey('fields', $resourceObjectType->config); + + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); + $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); + $resourceObjectType->config['fields'](); + } + public function testGetResourceObjectTypeCustomMutationInputArgs(): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); diff --git a/src/GraphQl/Tests/Type/TypeConverterTest.php b/src/GraphQl/Tests/Type/TypeConverterTest.php index 0ad0bac50ef..742158e4b4a 100644 --- a/src/GraphQl/Tests/Type/TypeConverterTest.php +++ b/src/GraphQl/Tests/Type/TypeConverterTest.php @@ -161,7 +161,7 @@ public function testConvertTypeInputResource(): void $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('dummy', $graphqlResourceMetadata, $operation, true, false, 1, true)->shouldBeCalled()->willReturn($expectedGraphqlType); $graphqlType = $this->typeConverter->convertType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1); $this->assertSame($expectedGraphqlType, $graphqlType); @@ -179,7 +179,7 @@ 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('dummyValue', $graphqlResourceMetadata, $collectionOperation, false, false, 0, true)->shouldBeCalled()->willReturn($expectedGraphqlType); /** @var Operation $rootOperation */ $rootOperation = (new Query())->withName('test'); diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 812565d1998..985a79d8ad2 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -48,7 +48,7 @@ public function __construct(private readonly TypesContainerInterface $typesConta /** * {@inheritdoc} */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0, bool $required = true): GraphQLType { $shortName = $operation->getShortName(); $operationName = $operation->getName(); @@ -86,8 +86,8 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo if ($this->typesContainer->has($shortName)) { $resourceObjectType = $this->typesContainer->get($shortName); - if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull)) { - throw new \LogicException(sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class]))); + if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull || $resourceObjectType instanceof InputObjectType)) { + throw new \LogicException(sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class, InputObjectType::class]))); } return $resourceObjectType; @@ -156,7 +156,15 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo 'interfaces' => $wrapData ? [] : [$this->getNodeInterface()], ]; - $resourceObjectType = $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration); + if ($input) { + $resourceObjectType = new InputObjectType($configuration); + if ($required) { + $resourceObjectType = GraphQLType::nonNull($resourceObjectType); + } + } else { + $resourceObjectType = new ObjectType($configuration); + } + $this->typesContainer->set($shortName, $resourceObjectType); return $resourceObjectType; diff --git a/src/GraphQl/Type/TypeBuilderEnumInterface.php b/src/GraphQl/Type/TypeBuilderEnumInterface.php index 9bbaa5215b0..750acf5e687 100644 --- a/src/GraphQl/Type/TypeBuilderEnumInterface.php +++ b/src/GraphQl/Type/TypeBuilderEnumInterface.php @@ -16,8 +16,6 @@ use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; use Symfony\Component\PropertyInfo\Type; @@ -31,9 +29,9 @@ interface TypeBuilderEnumInterface /** * Gets the object type of the given resource. * - * @return ObjectType|NonNull the object type, possibly wrapped by NonNull + * @return GraphQLType the object type, possibly wrapped by NonNull */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0, bool $required = false): GraphQLType; /** * Get the interface type of a node. diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 390ee7c5511..3ad7c142eac 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -182,7 +182,9 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati throw new OperationNotFoundException(); } - return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); + $required = $propertyMetadata?->isRequired() ?? true; + + return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth, $required); } private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType