Skip to content

Commit 414f297

Browse files
authored
Implement the Relay specification for mutations (#1597)
1 parent 7113a05 commit 414f297

File tree

4 files changed

+54
-27
lines changed

4 files changed

+54
-27
lines changed

features/graphql/mutation.feature

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,22 @@ Feature: GraphQL mutation support
2727
And the response should be in JSON
2828
And the header "Content-Type" should be equal to "application/json"
2929
And the JSON node "data.__type.fields[0].name" should contain "delete"
30-
And the JSON node "data.__type.fields[0].description" should contain "Deletes "
31-
And the JSON node "data.__type.fields[0].type.name" should contain "DeleteMutation"
30+
And the JSON node "data.__type.fields[0].description" should match '/^Deletes a [A-z]+\.$/'
31+
And the JSON node "data.__type.fields[0].type.name" should match "/^delete[A-z]+Payload$/"
3232
And the JSON node "data.__type.fields[0].type.kind" should be equal to "OBJECT"
3333
And the JSON node "data.__type.fields[0].args[0].name" should be equal to "input"
34-
And the JSON node "data.__type.fields[0].args[0].type.name" should contain "InputDeleteMutation"
34+
And the JSON node "data.__type.fields[0].args[0].type.name" should match "/^delete[A-z]+Input$/"
3535
And the JSON node "data.__type.fields[0].args[0].type.kind" should be equal to "INPUT_OBJECT"
3636

3737
Scenario: Create an item
3838
When I send the following GraphQL request:
3939
"""
4040
mutation {
41-
createFoo(input: {name: "A new one", bar: "new"}) {
42-
id,
43-
name,
41+
createFoo(input: {name: "A new one", bar: "new", clientMutationId: "myId"}) {
42+
id
43+
name
4444
bar
45+
clientMutationId
4546
}
4647
}
4748
"""
@@ -51,21 +52,24 @@ Feature: GraphQL mutation support
5152
And the JSON node "data.createFoo.id" should be equal to "/foos/1"
5253
And the JSON node "data.createFoo.name" should be equal to "A new one"
5354
And the JSON node "data.createFoo.bar" should be equal to "new"
55+
And the JSON node "data.createFoo.clientMutationId" should be equal to "myId"
5456

5557
@dropSchema
5658
Scenario: Delete an item through a mutation
5759
When I send the following GraphQL request:
5860
"""
5961
mutation {
60-
deleteFoo(input: {id: "/foos/1"}) {
62+
deleteFoo(input: {id: "/foos/1", clientMutationId: "anotherId"}) {
6163
id
64+
clientMutationId
6265
}
6366
}
6467
"""
6568
Then the response status code should be 200
6669
And the response should be in JSON
6770
And the header "Content-Type" should be equal to "application/json"
6871
And the JSON node "data.deleteFoo.id" should be equal to "/foos/1"
72+
And the JSON node "data.deleteFoo.clientMutationId" should be equal to "anotherId"
6973

7074
@createSchema
7175
@dropSchema
@@ -74,15 +78,17 @@ Feature: GraphQL mutation support
7478
When I send the following GraphQL request:
7579
"""
7680
mutation {
77-
deleteCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=1"}) {
81+
deleteCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=1", clientMutationId: "myId"}) {
7882
id
83+
clientMutationId
7984
}
8085
}
8186
"""
8287
Then the response status code should be 200
8388
And the response should be in JSON
8489
And the header "Content-Type" should be equal to "application/json"
8590
And the JSON node "data.deleteCompositeRelation.id" should be equal to "/composite_relations/compositeItem=1;compositeLabel=1"
91+
And the JSON node "data.deleteCompositeRelation.clientMutationId" should be equal to "myId"
8692

8793
@createSchema
8894
@dropSchema
@@ -91,10 +97,11 @@ Feature: GraphQL mutation support
9197
When I send the following GraphQL request:
9298
"""
9399
mutation {
94-
updateFoo(input: {id: "/foos/1", bar: "Modified description."}) {
95-
id,
96-
name,
100+
updateFoo(input: {id: "/foos/1", bar: "Modified description.", clientMutationId: "myId"}) {
101+
id
102+
name
97103
bar
104+
clientMutationId
98105
}
99106
}
100107
"""
@@ -104,6 +111,7 @@ Feature: GraphQL mutation support
104111
And the JSON node "data.updateFoo.id" should be equal to "/foos/1"
105112
And the JSON node "data.updateFoo.name" should be equal to "Hawsepipe"
106113
And the JSON node "data.updateFoo.bar" should be equal to "Modified description."
114+
And the JSON node "data.updateFoo.clientMutationId" should be equal to "myId"
107115

108116
@createSchema
109117
@dropSchema
@@ -112,9 +120,10 @@ Feature: GraphQL mutation support
112120
When I send the following GraphQL request:
113121
"""
114122
mutation {
115-
updateCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=2", value: "Modified value."}) {
123+
updateCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=2", value: "Modified value.", clientMutationId: "myId"}) {
116124
id
117125
value
126+
clientMutationId
118127
}
119128
}
120129
"""
@@ -123,3 +132,4 @@ Feature: GraphQL mutation support
123132
And the header "Content-Type" should be equal to "application/json"
124133
And the JSON node "data.updateCompositeRelation.id" should be equal to "/composite_relations/compositeItem=1;compositeLabel=2"
125134
And the JSON node "data.updateCompositeRelation.value" should be equal to "Modified value."
135+
And the JSON node "data.updateCompositeRelation.clientMutationId" should be equal to "myId"

src/Graphql/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ public function __construct(IriConverterInterface $iriConverter, DataPersisterIn
5353
public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
5454
{
5555
return function ($root, $args, $context, ResolveInfo $info) use ($resourceClass, $operationName) {
56+
$data = ['clientMutationId' => $args['input']['clientMutationId'] ?? null];
5657
$item = null;
58+
5759
if (isset($args['input']['id'])) {
5860
try {
5961
$item = $this->iriConverter->getItemFromIri($args['input']['id']);
@@ -75,13 +77,18 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
7577
$item,
7678
ItemNormalizer::FORMAT,
7779
$resourceMetadata->getGraphqlAttribute($operationName, 'normalization_context', [], true)
78-
);
80+
) + $data;
7981

8082
case 'delete':
81-
$this->dataPersister->remove($item);
82-
83-
return $args['input'];
83+
if ($item) {
84+
$this->dataPersister->remove($item);
85+
$data['id'] = $args['input']['id'];
86+
} else {
87+
$data['id'] = null;
88+
}
8489
}
90+
91+
return $data;
8592
};
8693
}
8794
}

src/Graphql/Type/SchemaBuilder.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,16 +294,18 @@ private function convertType(Type $type, bool $input = false, string $mutationNa
294294
*/
295295
private function getResourceObjectType(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null): GraphQLType
296296
{
297-
$shortName = $resourceMetadata->getShortName();
298-
if ($input) {
299-
$shortName .= 'Input';
297+
if (isset($this->graphqlTypes[$resourceClass][$mutationName][$input])) {
298+
return $this->graphqlTypes[$resourceClass][$mutationName][$input];
300299
}
300+
301+
$shortName = $resourceMetadata->getShortName();
301302
if (null !== $mutationName) {
302-
$shortName .= ucfirst($mutationName).'Mutation';
303+
$shortName = $mutationName.ucfirst($shortName);
303304
}
304-
305-
if (isset($this->graphqlTypes[$resourceClass][$mutationName][$input])) {
306-
return $this->graphqlTypes[$resourceClass][$mutationName][$input];
305+
if ($input) {
306+
$shortName .= 'Input';
307+
} elseif (null !== $mutationName) {
308+
$shortName .= 'Payload';
307309
}
308310

309311
$configuration = [
@@ -325,10 +327,14 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $
325327
private function getResourceObjectTypeFields(string $resource, bool $input = false, string $mutationName = null): array
326328
{
327329
$fields = [];
328-
$idField = ['type' => GraphQLType::id()];
330+
$idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
331+
$clientMutationId = GraphQLType::nonNull(GraphQLType::string());
329332

330333
if ('delete' === $mutationName) {
331-
return ['id' => $idField];
334+
return [
335+
'id' => $idField,
336+
'clientMutationId' => $clientMutationId,
337+
];
332338
}
333339

334340
if (!$input || 'create' !== $mutationName) {
@@ -350,6 +356,10 @@ private function getResourceObjectTypeFields(string $resource, bool $input = fal
350356
}
351357
}
352358

359+
if (null !== $mutationName) {
360+
$fields['clientMutationId'] = $clientMutationId;
361+
}
362+
353363
return $fields;
354364
}
355365

tests/Graphql/Resolver/Factory/ItemMutationResolverFactoryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function testCreateItemMutationResolverNoItem()
4444
$resolverFactory = $this->createItemMutationResolverFactory(null, $dataPersisterProphecy);
4545
$resolver = $resolverFactory(Dummy::class, Dummy::class, 'delete');
4646

47-
$resolver(null, ['input' => ['id' => '/dummies/3']], null, new ResolveInfo([]));
47+
$resolver(null, ['input' => ['id' => '/dummies/3', 'clientMutationId' => '1936']], null, new ResolveInfo([]));
4848
}
4949

5050
public function testCreateItemDeleteMutationResolver()
@@ -56,7 +56,7 @@ public function testCreateItemDeleteMutationResolver()
5656
$resolverFactory = $this->createItemMutationResolverFactory($dummy, $dataPersisterProphecy);
5757
$resolver = $resolverFactory(Dummy::class, null, 'delete');
5858

59-
$this->assertEquals(['id' => '/dummies/3'], $resolver(null, ['input' => ['id' => '/dummies/3']], null, new ResolveInfo([])));
59+
$this->assertEquals(['id' => '/dummies/3', 'clientMutationId' => '1936'], $resolver(null, ['input' => ['id' => '/dummies/3', 'clientMutationId' => '1936']], null, new ResolveInfo([])));
6060
}
6161

6262
private function createItemMutationResolverFactory($item, ObjectProphecy $dataPersisterProphecy): ResolverFactoryInterface

0 commit comments

Comments
 (0)