Skip to content

Commit 98b0e59

Browse files
mahmoodbazdaralanpoulain
authored andcommitted
[GraphQL] Adding form/multipart support to GraphQL entry point (#3041)
* Adding form/multipart support to GraphQL entry point so user can create custom file upload mutation * Applying requested review by @alanpoulain * Applying requested review: - using Symfony property accessor instead of using references * fixing php cs * Adding form/multipart support to GraphQL entry point so user can create custom file upload mutation * Applying requested review by @alanpoulain * Applying requested review: - using Symfony property accessor instead of using references * fixing php cs * Applying requested review from @alanpoulain * returning error when multipart form map does not match the variables * Fixes * Changelog * Behat tests
1 parent 97f1c61 commit 98b0e59

File tree

20 files changed

+596
-54
lines changed

20 files changed

+596
-54
lines changed

CHANGELOG.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.5.0 beta 2
4+
5+
* GraphQL: Add support for multipart request so user can create custom file upload mutations (#3041)
6+
37
## 2.5.0 beta 1
48

59
* Add an HTTP client dedicated to functional API testing (#2608)
@@ -22,15 +26,15 @@
2226
* Order filter now documents `asc`/`desc` as enum (#2971)
2327
* GraphQL: **BC Break** Separate `query` resource operation attribute into `item_query` and `collection_query` operations so user can use different security and serialization groups for them (#2944, #3015)
2428
* GraphQL: Add support for custom queries and mutations (#2447)
25-
* GraphQL: Add support for custom types
29+
* GraphQL: Add support for custom types (#2492)
2630
* GraphQL: Better pagination support (backwards pagination) (#2142)
27-
* GraphQL: Support the pagination per resource
31+
* GraphQL: Support the pagination per resource (#3035)
2832
* GraphQL: Add the concept of *stages* in the workflow of the resolvers and add the possibility to disable them with operation attributes (#2959)
29-
* GraphQL: Add GraphQL Playground besides GraphiQL and add the possibility to change the default IDE (or to disable it) for the GraphQL endpoint
33+
* GraphQL: Add GraphQL Playground besides GraphiQL and add the possibility to change the default IDE (or to disable it) for the GraphQL endpoint (#2956, #2961)
3034
* GraphQL: Add a command to print the schema in SDL `api:graphql:export > schema.graphql` (#2600)
31-
* GraphQL: Improve serialization performance by avoiding calls to the `serialize` PHP function
32-
* GraphQL: Allow to use a search and an exist filter on the same resource
33-
* GraphQL: Refactor the architecture of the whole system to allow the decoration of useful services (`TypeConverter` to manage custom types, `SerializerContextBuilder` to modify the (de)serialization context dynamically, etc.)
35+
* GraphQL: Improve serialization performance by avoiding calls to the `serialize` PHP function (#2576)
36+
* GraphQL: Allow to use a search and an exist filter on the same resource (#2243)
37+
* GraphQL: Refactor the architecture of the whole system to allow the decoration of useful services (`TypeConverter` to manage custom types, `SerializerContextBuilder` to modify the (de)serialization context dynamically, etc.) (#2772)
3438

3539
Notes:
3640

behat.yml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ default:
3030
bootstrap: 'tests/Fixtures/app/bootstrap.php'
3131
'Behat\MinkExtension':
3232
base_url: 'http://example.com/'
33+
files_path: 'features/files'
3334
sessions:
3435
default:
3536
symfony2: ~

features/bootstrap/GraphqlContext.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
1616
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
1717
use Behat\Gherkin\Node\PyStringNode;
18+
use Behat\Gherkin\Node\TableNode;
1819
use Behatch\Context\RestContext;
1920
use Behatch\HttpCall\Request;
2021
use GraphQL\Type\Introspection;
@@ -97,6 +98,45 @@ public function ISendTheGraphqlRequestWithOperation(string $operation)
9798
$this->sendGraphqlRequest();
9899
}
99100

101+
/**
102+
* @Given I have the following file(s) for a GraphQL request:
103+
*/
104+
public function iHaveTheFollowingFilesForAGraphqlRequest(TableNode $table)
105+
{
106+
$files = [];
107+
108+
foreach ($table->getHash() as $row) {
109+
if (!isset($row['name'], $row['file'])) {
110+
throw new \InvalidArgumentException('You must provide a "name" and "file" column in your table node.');
111+
}
112+
113+
$files[$row['name']] = $this->restContext->getMinkParameter('files_path').DIRECTORY_SEPARATOR.$row['file'];
114+
}
115+
116+
$this->graphqlRequest['files'] = $files;
117+
}
118+
119+
/**
120+
* @Given I have the following GraphQL multipart request map:
121+
*/
122+
public function iHaveTheFollowingGraphqlMultipartRequestMap(PyStringNode $string)
123+
{
124+
$this->graphqlRequest['map'] = $string->getRaw();
125+
}
126+
127+
/**
128+
* @When I send the following GraphQL multipart request operations:
129+
*/
130+
public function iSendTheFollowingGraphqlMultipartRequestOperations(PyStringNode $string)
131+
{
132+
$params = [];
133+
$params['operations'] = $string->getRaw();
134+
$params['map'] = $this->graphqlRequest['map'];
135+
136+
$this->request->setHttpHeader('Content-type', 'multipart/form-data');
137+
$this->request->send('POST', '/graphql', $params, $this->graphqlRequest['files']);
138+
}
139+
100140
/**
101141
* @When I send the query to introspect the schema
102142
*/

features/files/test.gif

35 Bytes
Loading

features/graphql/introspection.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Feature: GraphQL introspection support
66
Then the response status code should be 400
77
And the response should be in JSON
88
And the header "Content-Type" should be equal to "application/json"
9-
And the JSON node "errors[0].message" should be equal to "GraphQL query is not valid"
9+
And the JSON node "errors[0].message" should be equal to "GraphQL query is not valid."
1010

1111
Scenario: Introspect the GraphQL schema
1212
When I send the query to introspect the schema

features/graphql/mutation.feature

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Feature: GraphQL mutation support
2+
23
@createSchema
34
Scenario: Introspect types
45
When I send the following GraphQL request:
@@ -30,14 +31,14 @@ Feature: GraphQL mutation support
3031
Then the response status code should be 200
3132
And the response should be in JSON
3233
And the header "Content-Type" should be equal to "application/json"
33-
And the JSON node "data.__type.fields[0].name" should contain "delete"
34-
And the JSON node "data.__type.fields[0].description" should match '/^Deletes a [A-z0-9]+.$/'
35-
And the JSON node "data.__type.fields[0].type.name" should match "/^delete[A-z0-9]+Payload$/"
36-
And the JSON node "data.__type.fields[0].type.kind" should be equal to "OBJECT"
37-
And the JSON node "data.__type.fields[0].args[0].name" should be equal to "input"
38-
And the JSON node "data.__type.fields[0].args[0].type.kind" should be equal to "NON_NULL"
39-
And the JSON node "data.__type.fields[0].args[0].type.ofType.name" should match "/^delete[A-z0-9]+Input$/"
40-
And the JSON node "data.__type.fields[0].args[0].type.ofType.kind" should be equal to "INPUT_OBJECT"
34+
And the JSON node "data.__type.fields[2].name" should contain "delete"
35+
And the JSON node "data.__type.fields[2].description" should match '/^Deletes a [A-z0-9]+.$/'
36+
And the JSON node "data.__type.fields[2].type.name" should match "/^delete[A-z0-9]+Payload$/"
37+
And the JSON node "data.__type.fields[2].type.kind" should be equal to "OBJECT"
38+
And the JSON node "data.__type.fields[2].args[0].name" should be equal to "input"
39+
And the JSON node "data.__type.fields[2].args[0].type.kind" should be equal to "NON_NULL"
40+
And the JSON node "data.__type.fields[2].args[0].type.ofType.name" should match "/^delete[A-z0-9]+Input$/"
41+
And the JSON node "data.__type.fields[2].args[0].type.ofType.kind" should be equal to "INPUT_OBJECT"
4142

4243
Scenario: Create an item
4344
When I send the following GraphQL request:
@@ -420,3 +421,56 @@ Feature: GraphQL mutation support
420421
And the header "Content-Type" should be equal to "application/json"
421422
And the JSON node "data.testCustomArgumentsDummyCustomMutation.dummyCustomMutation.result" should be equal to "18"
422423
And the JSON node "data.testCustomArgumentsDummyCustomMutation.clientMutationId" should be equal to "myId"
424+
425+
Scenario: Uploading a file with a custom mutation
426+
Given I have the following file for a GraphQL request:
427+
| name | file |
428+
| file | test.gif |
429+
And I have the following GraphQL multipart request map:
430+
"""
431+
{
432+
"file": ["variables.file"]
433+
}
434+
"""
435+
When I send the following GraphQL multipart request operations:
436+
"""
437+
{
438+
"query": "mutation($file: Upload!) { uploadMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } }",
439+
"variables": {
440+
"file": null
441+
}
442+
}
443+
"""
444+
Then the response status code should be 200
445+
And the response should be in JSON
446+
And the JSON node "data.uploadMediaObject.mediaObject.contentUrl" should be equal to "test.gif"
447+
448+
Scenario: Uploading multiple files with a custom mutation
449+
Given I have the following files for a GraphQL request:
450+
| name | file |
451+
| 0 | test.gif |
452+
| 1 | test.gif |
453+
| 2 | test.gif |
454+
And I have the following GraphQL multipart request map:
455+
"""
456+
{
457+
"0": ["variables.files.0"],
458+
"1": ["variables.files.1"],
459+
"2": ["variables.files.2"]
460+
}
461+
"""
462+
When I send the following GraphQL multipart request operations:
463+
"""
464+
{
465+
"query": "mutation($files: [Upload!]!) { uploadMultipleMediaObject(input: {files: $files}) { mediaObject { id contentUrl } } }",
466+
"variables": {
467+
"files": [
468+
null,
469+
null,
470+
null
471+
]
472+
}
473+
}
474+
"""
475+
Then the response status code should be 200
476+
And the JSON node "data.uploadMultipleMediaObject.mediaObject.contentUrl" should be equal to "test.gif"

features/security/validate_incoming_content-types.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ Feature: Validate incoming content type
1313
"""
1414
Then the response status code should be 415
1515
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
16-
And the JSON node "hydra:description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html".'
16+
And the JSON node "hydra:description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'

features/security/validate_response_types.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Feature: Validate response types
88
And I send a "GET" request to "/dummies"
99
Then the response status code should be 406
1010
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
11-
And the JSON node "detail" should be equal to 'Requested format "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html".'
11+
And the JSON node "detail" should be equal to 'Requested format "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'
1212

1313
Scenario: Requesting a different format in the Accept header and in the URL should error
1414
When I add "Accept" header equal to "text/xml"
@@ -22,7 +22,7 @@ Feature: Validate response types
2222
And I send a "GET" request to "/dummies/1"
2323
Then the response status code should be 406
2424
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
25-
And the JSON node "detail" should be equal to 'Requested format "invalid" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html".'
25+
And the JSON node "detail" should be equal to 'Requested format "invalid" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'
2626

2727
Scenario: Requesting an invalid format in the URL should throw an error
2828
And I send a "GET" request to "/dummies/1.invalid"

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
<tag name="api_platform.graphql.type" />
103103
</service>
104104

105+
<service id="api_platform.graphql.upload_type" class="ApiPlatform\Core\GraphQl\Type\Definition\UploadType">
106+
<tag name="api_platform.graphql.type" />
107+
</service>
108+
105109
<service id="api_platform.graphql.type_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
106110
<tag name="container.service_locator" />
107111
</service>

0 commit comments

Comments
 (0)