Skip to content

Commit e91f362

Browse files
alanpoulaindunglas
authored andcommitted
[GraphQL] Serialization (#1945)
* Denormalize in ItemMutationResolverFactory using GraphQL denormalization context * Manage graphql_operation_name when serializing * Unit tests * Behat tests
1 parent 338e445 commit e91f362

File tree

8 files changed

+145
-15
lines changed

8 files changed

+145
-15
lines changed

features/graphql/introspection.feature

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,70 @@ Feature: GraphQL introspection support
187187
And the JSON node "data.__type.fields[9].name" should be equal to "jsonData"
188188
And the JSON node "data.__type.fields[9].type.name" should be equal to "Iterable"
189189

190+
Scenario: Retrieve entity - using serialization groups - fields
191+
When I send the following GraphQL request:
192+
"""
193+
{
194+
typeQuery: __type(name: "DummyGroup") {
195+
description,
196+
fields {
197+
name
198+
type {
199+
name
200+
kind
201+
ofType {
202+
name
203+
kind
204+
}
205+
}
206+
}
207+
}
208+
typeCreateInput: __type(name: "createDummyGroupInput") {
209+
description,
210+
inputFields {
211+
name
212+
type {
213+
name
214+
kind
215+
ofType {
216+
name
217+
kind
218+
}
219+
}
220+
}
221+
}
222+
typeCreatePayload: __type(name: "createDummyGroupPayload") {
223+
description,
224+
fields {
225+
name
226+
type {
227+
name
228+
kind
229+
ofType {
230+
name
231+
kind
232+
}
233+
}
234+
}
235+
}
236+
}
237+
"""
238+
Then the response status code should be 200
239+
And the response should be in JSON
240+
And the header "Content-Type" should be equal to "application/json"
241+
And the JSON node "data.typeQuery.fields" should have 2 elements
242+
And the JSON node "data.typeQuery.fields[0].name" should be equal to "id"
243+
And the JSON node "data.typeQuery.fields[1].name" should be equal to "foo"
244+
And the JSON node "data.typeCreateInput.inputFields" should have 3 elements
245+
And the JSON node "data.typeCreateInput.inputFields[0].name" should be equal to "bar"
246+
And the JSON node "data.typeCreateInput.inputFields[1].name" should be equal to "baz"
247+
And the JSON node "data.typeCreateInput.inputFields[2].name" should be equal to "clientMutationId"
248+
And the JSON node "data.typeCreatePayload.fields" should have 4 elements
249+
And the JSON node "data.typeCreatePayload.fields[0].name" should be equal to "id"
250+
And the JSON node "data.typeCreatePayload.fields[1].name" should be equal to "bar"
251+
And the JSON node "data.typeCreatePayload.fields[2].name" should be equal to "baz"
252+
And the JSON node "data.typeCreatePayload.fields[3].name" should be equal to "clientMutationId"
253+
190254
@dropSchema
191255
Scenario: Retrieve an item through a GraphQL query
192256
Given there are 4 dummy objects with relatedDummy

features/graphql/mutation.feature

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,27 @@ Feature: GraphQL mutation support
221221
And the JSON node "data.updateWritableId.name" should be equal to "Foo"
222222
And the JSON node "data.updateWritableId.clientMutationId" should be equal to "m"
223223

224+
Scenario: Use serialization groups
225+
Given there are 1 dummy group objects
226+
When I send the following GraphQL request:
227+
"""
228+
mutation {
229+
createDummyGroup(input: {bar: "Bar", baz: "Baz", clientMutationId: "myId"}) {
230+
id
231+
bar
232+
baz
233+
clientMutationId
234+
}
235+
}
236+
"""
237+
Then the response status code should be 200
238+
And the response should be in JSON
239+
And the header "Content-Type" should be equal to "application/json"
240+
And the JSON node "data.createDummyGroup.id" should be equal to "/dummy_groups/2"
241+
And the JSON node "data.createDummyGroup.bar" should be equal to "Bar"
242+
And the JSON node "data.createDummyGroup.baz" should be null
243+
And the JSON node "data.createDummyGroup.clientMutationId" should be equal to "myId"
244+
224245
@dropSchema
225246
Scenario: Trigger a validation error
226247
When I send the following GraphQL request:

features/graphql/query.feature

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ Feature: GraphQL query support
109109
And I send the GraphQL request with operation "DummyWithId1"
110110
And the JSON node "data.dummyItem.name" should be equal to "Dummy #1"
111111

112+
Scenario: Use serialization groups
113+
Given there are 1 dummy group objects
114+
When I send the following GraphQL request:
115+
"""
116+
{
117+
dummyGroup(id: "/dummy_groups/1") {
118+
foo
119+
}
120+
}
121+
"""
122+
Then the response status code should be 200
123+
And the response should be in JSON
124+
And the header "Content-Type" should be equal to "application/json"
125+
And the JSON node "data.dummyGroup.foo" should be equal to "Foo #1"
126+
112127
@dropSchema
113128
Scenario: Retrieve an nonexistent item through a GraphQL query
114129
When I send the following GraphQL request:

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
8686
case 'create':
8787
case 'update':
8888
$context = null === $item ? ['resource_class' => $resourceClass] : ['resource_class' => $resourceClass, 'object_to_populate' => $item];
89+
$context += $resourceMetadata->getGraphqlAttribute($operationName, 'denormalization_context', [], true);
8990
$item = $this->normalizer->denormalize($args['input'], $resourceClass, ItemNormalizer::FORMAT, $context);
9091
$this->validate($item, $info, $resourceMetadata, $operationName);
9192
$this->dataPersister->persist($item);

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ private function getResourceObjectTypeFields(string $resourceClass, ResourceMeta
432432
}
433433

434434
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
435-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
435+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? 'query']);
436436
if (
437437
null === ($propertyType = $propertyMetadata->getType())
438438
|| (!$input && null === $mutationName && false === $propertyMetadata->isReadable())

src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ private function getEffectiveSerializerGroups(array $options, string $resourceCl
162162
} elseif (isset($options['item_operation_name'])) {
163163
$normalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'normalization_context', null, true);
164164
$denormalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'denormalization_context', null, true);
165+
} elseif (isset($options['graphql_operation_name'])) {
166+
$normalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'normalization_context', null, true);
167+
$denormalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'denormalization_context', null, true);
165168
} else {
166169
$normalizationContext = $resourceMetadata->getAttribute('normalization_context');
167170
$denormalizationContext = $resourceMetadata->getAttribute('denormalization_context');

tests/Fixtures/TestBundle/Entity/DummyGroup.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,26 @@
2424
*
2525
* @ORM\Entity
2626
*
27-
* @ApiResource(attributes={
28-
* "normalization_context"={"groups"={"dummy_read"}},
29-
* "denormalization_context"={"groups"={"dummy_write"}},
30-
* "filters"={
31-
* "dummy_group.group",
32-
* "dummy_group.override_group",
33-
* "dummy_group.whitelist_group",
34-
* "dummy_group.override_whitelist_group"
27+
* @ApiResource(
28+
* attributes={
29+
* "normalization_context"={"groups"={"dummy_read"}},
30+
* "denormalization_context"={"groups"={"dummy_write"}},
31+
* "filters"={
32+
* "dummy_group.group",
33+
* "dummy_group.override_group",
34+
* "dummy_group.whitelist_group",
35+
* "dummy_group.override_whitelist_group"
36+
* }
37+
* },
38+
* graphql={
39+
* "query"={"normalization_context"={"groups"={"dummy_foo"}}},
40+
* "delete",
41+
* "create"={
42+
* "normalization_context"={"groups"={"dummy_bar"}},
43+
* "denormalization_context"={"groups"={"dummy_bar", "dummy_baz"}}
44+
* }
3545
* }
36-
* })
46+
* )
3747
*/
3848
class DummyGroup
3949
{

tests/GraphQl/Type/SchemaBuilderTest.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public function testGetSchema(bool $paginationEnabled)
112112
),
113113
"{$builtinType}Description",
114114
true,
115-
null,
115+
true,
116116
null,
117117
null,
118118
null
@@ -121,6 +121,7 @@ public function testGetSchema(bool $paginationEnabled)
121121
$mockedSchemaBuilder = $this->createSchemaBuilder($propertyMetadataMockBuilder, $paginationEnabled);
122122
$schema = $mockedSchemaBuilder->getSchema();
123123
$queryFields = $schema->getConfig()->getQuery()->getFields();
124+
$mutationFields = $schema->getConfig()->getMutation()->getFields();
124125

125126
$this->assertEquals([
126127
'node',
@@ -132,11 +133,17 @@ public function testGetSchema(bool $paginationEnabled)
132133
'shortName3s',
133134
], array_keys($queryFields));
134135

136+
$this->assertEquals([
137+
'createShortName1',
138+
'createShortName2',
139+
'createShortName3',
140+
], array_keys($mutationFields));
141+
135142
/** @var ObjectType $type */
136143
$type = $queryFields['shortName2']->getType();
137144
$resourceTypeFields = $type->getFields();
138145
$this->assertEquals(
139-
['id', 'intProperty', 'floatProperty', 'stringProperty', 'boolProperty', 'objectProperty', 'arrayProperty', 'iterableProperty'],
146+
['id', '_id', 'floatProperty', 'stringProperty', 'boolProperty', 'objectProperty', 'arrayProperty', 'iterableProperty'],
140147
array_keys($resourceTypeFields)
141148
);
142149

@@ -171,6 +178,14 @@ public function testGetSchema(bool $paginationEnabled)
171178
/** @var ListOfType $objectPropertyFieldType */
172179
$objectPropertyFieldType = $type->getFields()['objectProperty']->getType();
173180
$this->assertEquals(GraphQLType::nonNull(GraphQLType::string()), $objectPropertyFieldType);
181+
182+
/** @var ObjectType $type */
183+
$type = $mutationFields['createShortName2']->getType();
184+
$resourceTypeFields = $type->getFields();
185+
$this->assertEquals(
186+
['id', '_id', 'floatProperty', 'stringProperty', 'boolProperty', 'objectProperty', 'arrayProperty', 'iterableProperty', 'clientMutationId'],
187+
array_keys($resourceTypeFields)
188+
);
174189
}
175190

176191
public function paginationProvider(): array
@@ -202,16 +217,17 @@ private function createSchemaBuilder($propertyMetadataMockBuilder, bool $paginat
202217
null,
203218
null,
204219
null,
205-
['query' => []]
220+
['query' => [], 'create' => []]
206221
);
207222
$resourceMetadataFactoryProphecy->create($resourceClassName)->willReturn($resourceMetadata);
208223
$resourceMetadataFactoryProphecy->create('unknownResource')->willThrow(new ResourceClassNotFoundException());
209224

210225
$propertyNames = [];
211226
foreach (Type::$builtinTypes as $builtinType) {
212-
$propertyName = "{$builtinType}Property";
227+
$propertyName = Type::BUILTIN_TYPE_INT === $builtinType ? 'id' : "{$builtinType}Property";
213228
$propertyNames[] = $propertyName;
214-
$propertyMetadataFactoryProphecy->create($resourceClassName, $propertyName)->willReturn($propertyMetadataMockBuilder($builtinType, $resourceClassName));
229+
$propertyMetadataFactoryProphecy->create($resourceClassName, $propertyName, ['graphql_operation_name' => 'query'])->willReturn($propertyMetadataMockBuilder($builtinType, $resourceClassName));
230+
$propertyMetadataFactoryProphecy->create($resourceClassName, $propertyName, ['graphql_operation_name' => 'create'])->willReturn($propertyMetadataMockBuilder($builtinType, $resourceClassName));
215231
}
216232
$propertyNameCollection = new PropertyNameCollection($propertyNames);
217233
$propertyNameCollectionFactoryProphecy->create($resourceClassName)->willReturn($propertyNameCollection);

0 commit comments

Comments
 (0)