Skip to content

Commit ded6a64

Browse files
committed
feat(graphql): use denormalization groups to allow creation of relation in mutation
1 parent 288851e commit ded6a64

19 files changed

+411
-249
lines changed

features/graphql/mutation.feature

+16-3
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ Feature: GraphQL mutation support
425425
And the JSON node "data.createFoo.foo.name" should be equal to "Created without mutation id"
426426
And the JSON node "data.createFoo.foo.bar" should be equal to "works"
427427

428-
Scenario: Create an item with a subresource
428+
Scenario: Create an item with a relation to an existing resource
429429
Given there are 1 dummy objects with relatedDummy
430430
When I send the following GraphQL request:
431431
"""
@@ -666,15 +666,26 @@ Feature: GraphQL mutation support
666666
Scenario: Use serialization groups with relations
667667
Given there is 1 dummy object with relatedDummy and its thirdLevel
668668
And there is a RelatedDummy with 2 friends
669+
And there is a dummy object with a fourth level relation
669670
When I send the following GraphQL request:
670671
"""
671672
mutation {
672-
updateRelatedDummy(input: {id: "/related_dummies/2", symfony: "laravel", embeddedDummy: "{}", thirdLevel: "/third_levels/1"}) {
673+
updateRelatedDummy(input: {
674+
id: "/related_dummies/2",
675+
symfony: "laravel",
676+
thirdLevel: {
677+
fourthLevel: "/fourth_levels/1"
678+
}
679+
}) {
673680
relatedDummy {
674681
id
675682
symfony
676683
thirdLevel {
677684
id
685+
fourthLevel {
686+
id
687+
__typename
688+
}
678689
__typename
679690
}
680691
relatedToDummyFriend {
@@ -694,8 +705,10 @@ Feature: GraphQL mutation support
694705
And the header "Content-Type" should be equal to "application/json"
695706
And the JSON node "data.updateRelatedDummy.relatedDummy.id" should be equal to "/related_dummies/2"
696707
And the JSON node "data.updateRelatedDummy.relatedDummy.symfony" should be equal to "laravel"
697-
And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.id" should be equal to "/third_levels/1"
708+
And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.id" should be equal to "/third_levels/3"
698709
And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.__typename" should be equal to "updateThirdLevelNestedPayload"
710+
And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.id" should be equal to "/fourth_levels/1"
711+
And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.__typename" should be equal to "updateFourthLevelNestedPayload"
699712
And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.__typename" should be equal to "updateRelatedToDummyFriendNestedPayloadConnection"
700713
And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[0].node.name" should be equal to "Relation-1"
701714
And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[1].node.name" should be equal to "Relation-2"

src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
<argument type="service" id="api_platform.graphql.type_builder" />
131131
<argument type="service" id="api_platform.graphql.types_container" />
132132
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
133+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
133134
</service>
134135

135136
<service id="api_platform.graphql.type_builder" class="ApiPlatform\GraphQl\Type\TypeBuilder" public="false">

src/Core/Serializer/AbstractItemNormalizer.php

+3
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,9 @@ private function createAttributeValue($attribute, $value, $format = null, array
864864
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
865865
$childContext = $this->createChildContext($context, $attribute, $format);
866866
$childContext['resource_class'] = $resourceClass;
867+
if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
868+
$childContext['operation'] = $this->resourceMetadataFactory->create($resourceClass)->getOperation();
869+
}
867870

868871
return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
869872
}

src/GraphQl/Type/FieldsBuilder.php

+50-65
Large diffs are not rendered by default.

src/GraphQl/Type/FieldsBuilderInterface.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,30 @@ public function getNodeQueryFields(): array;
3232
/**
3333
* Gets the item query fields of the schema.
3434
*/
35-
public function getItemQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array;
35+
public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array;
3636

3737
/**
3838
* Gets the collection query fields of the schema.
3939
*/
40-
public function getCollectionQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array;
40+
public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array;
4141

4242
/**
4343
* Gets the mutation fields of the schema.
4444
*/
45-
public function getMutationFields(string $resourceClass, Operation $operation, string $mutationName): array;
45+
public function getMutationFields(string $resourceClass, Operation $operation): array;
4646

4747
/**
4848
* Gets the subscription fields of the schema.
4949
*/
50-
public function getSubscriptionFields(string $resourceClass, Operation $operation, string $subscriptionName): array;
50+
public function getSubscriptionFields(string $resourceClass, Operation $operation): array;
5151

5252
/**
5353
* Gets the fields of the type of the given resource.
5454
*/
55-
public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, string $operationName, int $depth = 0, ?array $ioMetadata = null): array;
55+
public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array;
5656

5757
/**
5858
* Resolve the args of a resource by resolving its types.
5959
*/
60-
public function resolveResourceArgs(array $args, string $operationName, string $shortName): array;
60+
public function resolveResourceArgs(array $args, Operation $operation): array;
6161
}

src/GraphQl/Type/SchemaBuilder.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -66,35 +66,35 @@ public function getSchema(): Schema
6666

6767
//TODO: 3.0 remove these
6868
if ('item_query' === $operationName) {
69-
$queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $operationName, $configuration);
69+
$queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration);
7070
continue;
7171
}
7272

7373
if ('collection_query' === $operationName) {
74-
$queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $operationName, $configuration);
74+
$queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $configuration);
7575

7676
continue;
7777
}
7878

7979
if ($operation instanceof Query && !$operation->isCollection()) {
80-
$queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $operationName, $configuration);
80+
$queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration);
8181

8282
continue;
8383
}
8484

8585
if ($operation instanceof Query && $operation->isCollection()) {
86-
$queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $operationName, $configuration);
86+
$queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $configuration);
8787

8888
continue;
8989
}
9090

9191
if ($operation instanceof Subscription && $operation->getMercure()) {
92-
$subscriptionFields += $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation, $operationName);
92+
$subscriptionFields += $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation);
9393

9494
continue;
9595
}
9696

97-
$mutationFields += $this->fieldsBuilder->getMutationFields($resourceClass, $operation, $operationName);
97+
$mutationFields += $this->fieldsBuilder->getMutationFields($resourceClass, $operation);
9898
}
9999
}
100100
}

src/GraphQl/Type/TypeBuilder.php

+17-13
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,10 @@ public function __construct(TypesContainerInterface $typesContainer, callable $d
5454
/**
5555
* {@inheritdoc}
5656
*/
57-
public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, string $operationName, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType
57+
public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType
5858
{
59-
try {
60-
$operation = $resourceMetadataCollection->getGraphQlOperation($operationName);
61-
} catch (OperationNotFoundException $e) {
62-
$operation = (new Query())
63-
->withResource($resourceMetadataCollection[0])
64-
->withName($operationName)
65-
->withCollection('collection_query' === $operationName);
66-
}
67-
6859
$shortName = $operation->getShortName();
60+
$operationName = $operation->getName();
6961

7062
if ($operation instanceof Mutation) {
7163
$shortName = $operationName.ucfirst($shortName);
@@ -76,6 +68,9 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
7668
}
7769

7870
if ($input) {
71+
if ($depth > 0) {
72+
$shortName .= 'Nested';
73+
}
7974
$shortName .= 'Input';
8075
} elseif ($operation instanceof Mutation || $operation instanceof Subscription) {
8176
if ($depth > 0) {
@@ -141,8 +136,17 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
141136
$wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
142137
}
143138

139+
try {
140+
$wrappedOperation = $resourceMetadataCollection->getGraphQlOperation($wrappedOperationName);
141+
} catch (OperationNotFoundException $e) {
142+
$wrappedOperation = (new Query())
143+
->withResource($resourceMetadataCollection[0])
144+
->withName($wrappedOperationName)
145+
->withCollection('collection_query' === $wrappedOperationName);
146+
}
147+
144148
$fields = [
145-
lcfirst($operation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperationName, $input, true, $depth),
149+
lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation, $input, true, $depth),
146150
];
147151

148152
if ($operation instanceof Subscription) {
@@ -158,10 +162,10 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
158162
}
159163

160164
$fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
161-
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $operationName, $depth, $ioMetadata);
165+
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata);
162166

163167
if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) {
164-
return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operationName, $operation->getShortName()) + ['clientMutationId' => $fields['clientMutationId']];
168+
return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']];
165169
}
166170

167171
return $fields;

src/GraphQl/Type/TypeBuilderInterface.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\GraphQl\Type;
1515

16+
use ApiPlatform\Metadata\GraphQl\Operation;
1617
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
1718
use GraphQL\Type\Definition\InterfaceType;
1819
use GraphQL\Type\Definition\NonNull;
@@ -34,7 +35,7 @@ interface TypeBuilderInterface
3435
*
3536
* @return ObjectType|NonNull the object type, possibly wrapped by NonNull
3637
*/
37-
public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, string $operationName, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType;
38+
public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType;
3839

3940
/**
4041
* Get the interface type of a node.

src/GraphQl/Type/TypeConverter.php

+38-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
namespace ApiPlatform\GraphQl\Type;
1515

1616
use ApiPlatform\Exception\InvalidArgumentException;
17+
use ApiPlatform\Exception\OperationNotFoundException;
1718
use ApiPlatform\Exception\ResourceClassNotFoundException;
19+
use ApiPlatform\Metadata\GraphQl\Operation;
20+
use ApiPlatform\Metadata\GraphQl\Query;
21+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1822
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
1923
use GraphQL\Error\SyntaxError;
2024
use GraphQL\Language\AST\ListTypeNode;
@@ -38,18 +42,24 @@ final class TypeConverter implements TypeConverterInterface
3842
private $typeBuilder;
3943
private $typesContainer;
4044
private $resourceMetadataCollectionFactory;
45+
private $propertyMetadataFactory;
4146

42-
public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInterface $typesContainer, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
47+
public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInterface $typesContainer, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory = null)
4348
{
4449
$this->typeBuilder = $typeBuilder;
4550
$this->typesContainer = $typesContainer;
4651
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
52+
$this->propertyMetadataFactory = $propertyMetadataFactory;
53+
54+
if (null === $this->propertyMetadataFactory) {
55+
@trigger_error(sprintf('Not injecting %s in the TypeConverter is deprecated since 2.7 and will not be supported in 3.0.', PropertyMetadataFactoryInterface::class), \E_USER_DEPRECATED);
56+
}
4757
}
4858

4959
/**
5060
* {@inheritdoc}
5161
*/
52-
public function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
62+
public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth)
5363
{
5464
switch ($type->getBuiltinType()) {
5565
case Type::BUILTIN_TYPE_BOOL:
@@ -62,21 +72,17 @@ public function convertType(Type $type, bool $input, string $operationName, stri
6272
return GraphQLType::string();
6373
case Type::BUILTIN_TYPE_ARRAY:
6474
case Type::BUILTIN_TYPE_ITERABLE:
65-
if ($resourceType = $this->getResourceType($type, $input, $operationName, $depth)) {
75+
if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) {
6676
return $resourceType;
6777
}
6878

6979
return 'Iterable';
7080
case Type::BUILTIN_TYPE_OBJECT:
71-
if ($input && $depth > 0) {
72-
return GraphQLType::string();
73-
}
74-
7581
if (is_a($type->getClassName(), \DateTimeInterface::class, true)) {
7682
return GraphQLType::string();
7783
}
7884

79-
return $this->getResourceType($type, $input, $operationName, $depth);
85+
return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
8086
default:
8187
return null;
8288
}
@@ -100,7 +106,7 @@ public function resolveType(string $type): ?GraphQLType
100106
throw new InvalidArgumentException(sprintf('The type "%s" was not resolved.', $type));
101107
}
102108

103-
private function getResourceType(Type $type, bool $input, string $operationName, int $depth): ?GraphQLType
109+
private function getResourceType(Type $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType
104110
{
105111
if (
106112
$this->typeBuilder->isCollection($type) &&
@@ -122,7 +128,6 @@ private function getResourceType(Type $type, bool $input, string $operationName,
122128
}
123129

124130
$hasGraphQl = false;
125-
$operation = null;
126131
foreach ($resourceMetadataCollection as $resourceMetadata) {
127132
if (null !== $resourceMetadata->getGraphQlOperations()) {
128133
$hasGraphQl = true;
@@ -138,7 +143,29 @@ private function getResourceType(Type $type, bool $input, string $operationName,
138143
return null;
139144
}
140145

141-
return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operationName, $input, false, $depth);
146+
$propertyMetadata = null;
147+
if ($property && $this->propertyMetadataFactory) {
148+
$context = [
149+
'normalization_groups' => $rootOperation->getNormalizationContext()['groups'] ?? null,
150+
'denormalization_groups' => $rootOperation->getDenormalizationContext()['groups'] ?? null,
151+
];
152+
$propertyMetadata = $this->propertyMetadataFactory->create($rootResource, $property, $context);
153+
}
154+
155+
if ($input && $depth > 0 && (!$propertyMetadata || !$propertyMetadata->isWritableLink())) {
156+
return GraphQLType::string();
157+
}
158+
159+
try {
160+
$operation = $resourceMetadataCollection->getGraphQlOperation($rootOperation->getName());
161+
} catch (OperationNotFoundException $e) {
162+
$operation = (new Query())
163+
->withResource($resourceMetadataCollection[0])
164+
->withName($rootOperation->getName())
165+
->withCollection('collection_query' === $rootOperation->getName());
166+
}
167+
168+
return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth);
142169
}
143170

144171
private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType

src/GraphQl/Type/TypeConverterInterface.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\GraphQl\Type;
1515

16+
use ApiPlatform\Metadata\GraphQl\Operation;
1617
use GraphQL\Type\Definition\Type as GraphQLType;
1718
use Symfony\Component\PropertyInfo\Type;
1819

@@ -31,7 +32,7 @@ interface TypeConverterInterface
3132
*
3233
* @return string|GraphQLType|null
3334
*/
34-
public function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth);
35+
public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth);
3536

3637
/**
3738
* Resolves a type written with the GraphQL type system to its object representation.

tests/Fixtures/TestBundle/Document/RelatedDummy.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* @author Kévin Dunglas <[email protected]>
2828
* @author Alexandre Delplace <[email protected]>
2929
*
30-
* @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.mongodb.friends"}})
30+
* @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}, "denormalization_context"={"groups"={"friends"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.mongodb.friends"}})
3131
* @ODM\Document
3232
*/
3333
class RelatedDummy extends ParentDummy

0 commit comments

Comments
 (0)