Skip to content

Commit 50a15af

Browse files
authored
Resolve types in custom operations (#2824)
1 parent 6e2c268 commit 50a15af

21 files changed

+478
-34
lines changed

features/graphql/collection.feature

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,78 @@ Feature: GraphQL collection support
482482
And the header "Content-Type" should be equal to "application/json"
483483
And the JSON node "data.dummies.edges" should have 0 element
484484

485+
Scenario: Custom collection query
486+
Given there are 2 dummyCustomQuery objects
487+
When I send the following GraphQL request:
488+
"""
489+
{
490+
testCollectionDummyCustomQueries {
491+
edges {
492+
node {
493+
message
494+
}
495+
}
496+
}
497+
}
498+
"""
499+
Then the response status code should be 200
500+
And the response should be in JSON
501+
And the header "Content-Type" should be equal to "application/json"
502+
And the JSON should be equal to:
503+
"""
504+
{
505+
"data": {
506+
"testCollectionDummyCustomQueries": {
507+
"edges": [
508+
{
509+
"node": {"message": "Success!"}
510+
},
511+
{
512+
"node": {"message": "Success!"}
513+
}
514+
]
515+
}
516+
}
517+
}
518+
"""
519+
520+
@createSchema
521+
Scenario: Custom collection query with custom arguments
522+
Given there are 2 dummyCustomQuery objects
523+
When I send the following GraphQL request:
524+
"""
525+
{
526+
testCollectionCustomArgumentsDummyCustomQueries(customArgumentString: "A string") {
527+
edges {
528+
node {
529+
message
530+
customArgs
531+
}
532+
}
533+
}
534+
}
535+
"""
536+
Then the response status code should be 200
537+
And the response should be in JSON
538+
And the header "Content-Type" should be equal to "application/json"
539+
And the JSON should be equal to:
540+
"""
541+
{
542+
"data": {
543+
"testCollectionCustomArgumentsDummyCustomQueries": {
544+
"edges": [
545+
{
546+
"node": {"message": "Success!", "customArgs": {"customArgumentString": "A string"}}
547+
},
548+
{
549+
"node": {"message": "Success!", "customArgs": {"customArgumentString": "A string"}}
550+
}
551+
]
552+
}
553+
}
554+
}
555+
"""
556+
485557
@!mongodb
486558
@createSchema
487559
Scenario: Retrieve an item with composite primitive identifiers through a GraphQL query

features/graphql/mutation.feature

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Feature: GraphQL mutation support
204204
When I send the following GraphQL request:
205205
"""
206206
mutation {
207-
updateDummy(input: {id: "/dummies/1", description: "Modified description.", dummyDate: "2018-06-05", clientMutationId: "myId"}) {
207+
updateDummy(input: {id: "/dummies/1", description: "Modified description.", dummyDate: "2018-06-05T00:00:00+00:00", clientMutationId: "myId"}) {
208208
dummy {
209209
id
210210
name
@@ -475,3 +475,21 @@ Feature: GraphQL mutation support
475475
And the response should be in JSON
476476
And the header "Content-Type" should be equal to "application/json"
477477
And the JSON node "data.sumNotPersistedDummyCustomMutation.dummyCustomMutation" should be null
478+
479+
Scenario: Execute a custom mutation with custom arguments
480+
When I send the following GraphQL request:
481+
"""
482+
mutation {
483+
testCustomArgumentsDummyCustomMutation(input: {operandC: 18, clientMutationId: "myId"}) {
484+
dummyCustomMutation {
485+
result
486+
}
487+
clientMutationId
488+
}
489+
}
490+
"""
491+
Then the response status code should be 200
492+
And the response should be in JSON
493+
And the header "Content-Type" should be equal to "application/json"
494+
And the JSON node "data.testCustomArgumentsDummyCustomMutation.dummyCustomMutation.result" should be equal to "18"
495+
And the JSON node "data.testCustomArgumentsDummyCustomMutation.clientMutationId" should be equal to "myId"

features/graphql/query.feature

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,22 @@ Feature: GraphQL query support
331331
}
332332
"""
333333

334-
Scenario: Custom collection query
334+
Scenario: Custom item query with custom arguments
335+
Given there are 2 dummyCustomQuery objects
335336
When I send the following GraphQL request:
336337
"""
337338
{
338-
testCollectionDummyCustomQueries {
339-
edges {
340-
node {
341-
message
342-
}
343-
}
339+
testItemCustomArgumentsDummyCustomQuery(
340+
id: "/dummy_custom_queries/1",
341+
customArgumentBool: true,
342+
customArgumentInt: 3,
343+
customArgumentString: "A string",
344+
customArgumentFloat: 2.6,
345+
customArgumentIntArray: [4],
346+
customArgumentCustomType: "2019-05-24T00:00:00+00:00"
347+
) {
348+
message
349+
customArgs
344350
}
345351
}
346352
"""
@@ -351,15 +357,17 @@ Feature: GraphQL query support
351357
"""
352358
{
353359
"data": {
354-
"testCollectionDummyCustomQueries": {
355-
"edges": [
356-
{
357-
"node": {"message": "Success!"}
358-
},
359-
{
360-
"node": {"message": "Success!"}
361-
}
362-
]
360+
"testItemCustomArgumentsDummyCustomQuery": {
361+
"message": "Success!",
362+
"customArgs": {
363+
"id": "/dummy_custom_queries/1",
364+
"customArgumentBool": true,
365+
"customArgumentInt": 3,
366+
"customArgumentString": "A string",
367+
"customArgumentFloat": 2.6,
368+
"customArgumentIntArray": [4],
369+
"customArgumentCustomType": "2019-05-24T00:00:00+00:00"
370+
}
363371
}
364372
}
365373
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969
<service id="api_platform.graphql.type_converter" class="ApiPlatform\Core\GraphQl\Type\TypeConverter">
7070
<argument type="service" id="api_platform.graphql.type_builder" />
71+
<argument type="service" id="api_platform.graphql.types_container" />
7172
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
7273
</service>
7374

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,16 @@ public function getQueryFields(string $resourceClass, ResourceMetadata $resource
8989
$deprecationReason = $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true);
9090

9191
if (false !== $itemConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) {
92-
$itemConfiguration['args'] = $itemConfiguration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]];
92+
$args = $this->resolveResourceArgs($itemConfiguration['args'] ?? [], $queryName, $shortName);
93+
$itemConfiguration['args'] = $args ?: $itemConfiguration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]];
9394

9495
$queryFields[$fieldName] = array_merge($fieldConfiguration, $itemConfiguration);
9596
}
9697

9798
if (false !== $collectionConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) {
99+
$args = $this->resolveResourceArgs($collectionConfiguration['args'] ?? [], $queryName, $shortName);
100+
$collectionConfiguration['args'] = $args ?: $collectionConfiguration['args'] ?? $fieldConfiguration['args'];
101+
98102
$queryFields[Inflector::pluralize($fieldName)] = array_merge($fieldConfiguration, $collectionConfiguration);
99103
}
100104

@@ -189,6 +193,22 @@ public function getResourceObjectTypeFields(?string $resourceClass, ResourceMeta
189193
return $fields;
190194
}
191195

196+
/**
197+
* {@inheritdoc}
198+
*/
199+
public function resolveResourceArgs(array $args, string $operationName, string $shortName): array
200+
{
201+
foreach ($args as $id => $arg) {
202+
if (!isset($arg['type'])) {
203+
throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operationName, $shortName));
204+
}
205+
206+
$args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
207+
}
208+
209+
return $args;
210+
}
211+
192212
/**
193213
* Get the field configuration of a resource.
194214
*

src/GraphQl/Type/FieldsBuilderInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@ public function getMutationFields(string $resourceClass, ResourceMetadata $resou
4646
* Gets the fields of the type of the given resource.
4747
*/
4848
public function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, int $depth, ?array $ioMetadata): array;
49+
50+
/**
51+
* Resolve the args of a resource by resolving its types.
52+
*/
53+
public function resolveResourceArgs(array $args, string $operationName, string $shortName): array;
4954
}

src/GraphQl/Type/TypeBuilder.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,15 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadata $
9595
];
9696
}
9797

98-
return $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder')->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata);
98+
$fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
99+
100+
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata);
101+
102+
if ($input && null !== $mutationName && null !== $mutationArgs = $resourceMetadata->getGraphql()[$mutationName]['args'] ?? null) {
103+
return $fieldsBuilder->resolveResourceArgs($mutationArgs, $mutationName, $resourceMetadata->getShortName()) + ['clientMutationId' => $fields['clientMutationId']];
104+
}
105+
106+
return $fields;
99107
},
100108
'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
101109
];

src/GraphQl/Type/TypeConverter.php

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,37 @@
1313

1414
namespace ApiPlatform\Core\GraphQl\Type;
1515

16+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1617
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
1718
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19+
use GraphQL\Error\SyntaxError;
20+
use GraphQL\Language\AST\ListTypeNode;
21+
use GraphQL\Language\AST\NamedTypeNode;
22+
use GraphQL\Language\AST\NonNullTypeNode;
23+
use GraphQL\Language\AST\TypeNode;
24+
use GraphQL\Language\Parser;
25+
use GraphQL\Type\Definition\NullableType;
1826
use GraphQL\Type\Definition\Type as GraphQLType;
1927
use Symfony\Component\PropertyInfo\Type;
2028

2129
/**
22-
* Convert a built-in type to its GraphQL equivalent.
30+
* Converts a type to its GraphQL equivalent.
2331
*
2432
* @experimental
2533
*
2634
* @author Alan Poulain <[email protected]>
2735
*/
2836
final class TypeConverter implements TypeConverterInterface
2937
{
30-
private $resourceMetadataFactory;
3138
private $typeBuilder;
39+
private $typesContainer;
40+
private $resourceMetadataFactory;
3241

33-
public function __construct(TypeBuilderInterface $typeBuilder, ResourceMetadataFactoryInterface $resourceMetadataFactory)
42+
public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInterface $typesContainer, ResourceMetadataFactoryInterface $resourceMetadataFactory)
3443
{
35-
$this->resourceMetadataFactory = $resourceMetadataFactory;
3644
$this->typeBuilder = $typeBuilder;
45+
$this->typesContainer = $typesContainer;
46+
$this->resourceMetadataFactory = $resourceMetadataFactory;
3747
}
3848

3949
/**
@@ -68,6 +78,24 @@ public function convertType(Type $type, bool $input, ?string $queryName, ?string
6878
}
6979
}
7080

81+
/**
82+
* {@inheritdoc}
83+
*/
84+
public function resolveType(string $type): ?GraphQLType
85+
{
86+
try {
87+
$astTypeNode = Parser::parseType($type);
88+
} catch (SyntaxError $e) {
89+
throw new InvalidArgumentException(sprintf('"%s" is not a valid GraphQL type.', $type), 0, $e);
90+
}
91+
92+
if ($graphQlType = $this->resolveAstTypeNode($astTypeNode, $type)) {
93+
return $graphQlType;
94+
}
95+
96+
throw new InvalidArgumentException(sprintf('The type "%s" was not resolved.', $type));
97+
}
98+
7199
private function getResourceType(Type $type, bool $input, ?string $queryName, ?string $mutationName, int $depth): ?GraphQLType
72100
{
73101
$resourceClass = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
@@ -87,4 +115,51 @@ private function getResourceType(Type $type, bool $input, ?string $queryName, ?s
87115

88116
return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, false, $depth);
89117
}
118+
119+
private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
120+
{
121+
if ($astTypeNode instanceof NonNullTypeNode) {
122+
/** @var NullableType|null $nullableAstTypeNode */
123+
$nullableAstTypeNode = $this->resolveNullableAstTypeNode($astTypeNode->type, $fromType);
124+
125+
return $nullableAstTypeNode ? GraphQLType::nonNull($nullableAstTypeNode) : null;
126+
}
127+
128+
return $this->resolveNullableAstTypeNode($astTypeNode, $fromType);
129+
}
130+
131+
private function resolveNullableAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
132+
{
133+
if ($astTypeNode instanceof ListTypeNode) {
134+
/** @var TypeNode $astTypeNodeElement */
135+
$astTypeNodeElement = $astTypeNode->type;
136+
137+
return GraphQLType::listOf($this->resolveAstTypeNode($astTypeNodeElement, $fromType));
138+
}
139+
140+
if (!$astTypeNode instanceof NamedTypeNode) {
141+
return null;
142+
}
143+
144+
$typeName = $astTypeNode->name->value;
145+
146+
switch ($typeName) {
147+
case GraphQLType::STRING:
148+
return GraphQLType::string();
149+
case GraphQLType::INT:
150+
return GraphQLType::int();
151+
case GraphQLType::BOOLEAN:
152+
return GraphQLType::boolean();
153+
case GraphQLType::FLOAT:
154+
return GraphQLType::float();
155+
case GraphQLType::ID:
156+
return GraphQLType::id();
157+
default:
158+
if ($this->typesContainer->has($typeName)) {
159+
return $this->typesContainer->get($typeName);
160+
}
161+
162+
return null;
163+
}
164+
}
90165
}

src/GraphQl/Type/TypeConverterInterface.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Symfony\Component\PropertyInfo\Type;
1818

1919
/**
20-
* Convert a built-in type to its GraphQL equivalent.
20+
* Converts a type to its GraphQL equivalent.
2121
*
2222
* @experimental
2323
*
@@ -26,7 +26,15 @@
2626
interface TypeConverterInterface
2727
{
2828
/**
29+
* Converts a built-in type to its GraphQL equivalent.
30+
* A string can be returned for a custom registered type.
31+
*
2932
* @return string|GraphQLType|null
3033
*/
3134
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth);
35+
36+
/**
37+
* Resolves a type written with the GraphQL type system to its object representation.
38+
*/
39+
public function resolveType(string $type): ?GraphQLType;
3240
}

tests/Fixtures/TestBundle/Document/DummyCustomMutation.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
* "mutation"="app.graphql.mutation_resolver.dummy_custom_not_persisted",
3232
* "normalization_context"={"groups"={"result"}},
3333
* "denormalization_context"={"groups"={"sum"}}
34+
* },
35+
* "testCustomArguments"={
36+
* "mutation"="app.graphql.mutation_resolver.dummy_custom",
37+
* "args"={"operandC"={"type"="Int!"}}
3438
* }
3539
* })
3640
*

0 commit comments

Comments
 (0)