diff --git a/Command/GraphDumpSchemaCommand.php b/Command/GraphDumpSchemaCommand.php index 76cf928a3..034a49310 100644 --- a/Command/GraphDumpSchemaCommand.php +++ b/Command/GraphDumpSchemaCommand.php @@ -49,18 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->execute($request) ->toArray(); - if (isset($result['errors'])) { - foreach ($result['errors'] as $error) { - $output->error($error['message']); - } - - return 1; - } - - $file = $input->getOption('file'); - if (empty($file)) { - $file = $container->getParameter('kernel.root_dir').'/../var/schema.json'; - } + $file = $input->hasOption('file') ? $input->getOption('file') : $container->getParameter('kernel.root_dir').'/../var/schema.json'; $schema = json_encode($result['data']); diff --git a/Controller/GraphController.php b/Controller/GraphController.php index b7c377f86..cafd9b3cc 100644 --- a/Controller/GraphController.php +++ b/Controller/GraphController.php @@ -19,9 +19,32 @@ class GraphController extends Controller { public function endpointAction(Request $request) { - $req = $this->get('overblog_graphql.request_parser')->parse($request); - $res = $this->get('overblog_graphql.request_executor')->execute($req, []); + if ($request->query->has('batch')) { + $data = $this->treatBatchQuery($request); + } else { + $data = $this->treatNormalQuery($request); + } - return new JsonResponse($res->toArray(), 200); + return new JsonResponse($data, 200); + } + + private function treatBatchQuery(Request $request) + { + $params = $this->get('overblog_graphql.request_batch_parser')->parse($request); + $data = []; + + foreach ($params as $i => $entry) { + $data[$i] = $this->get('overblog_graphql.request_executor')->execute($entry)->toArray(); + } + + return $data; + } + + private function treatNormalQuery(Request $request) + { + $params = $this->get('overblog_graphql.request_parser')->parse($request); + $data = $this->get('overblog_graphql.request_executor')->execute($params)->toArray(); + + return $data; } } diff --git a/Relay/Mutation/InputType.php b/Relay/Mutation/InputType.php index c1bfbe084..0761cf6ca 100644 --- a/Relay/Mutation/InputType.php +++ b/Relay/Mutation/InputType.php @@ -46,9 +46,6 @@ public function __construct(array $config) ); $name = str_replace('Input', '', $config['name']); - if (empty($name)) { - $name = $config['name']; - } parent::__construct([ 'name' => $name.'Input', diff --git a/Relay/Mutation/PayloadType.php b/Relay/Mutation/PayloadType.php index 53033841e..a5423e9a0 100644 --- a/Relay/Mutation/PayloadType.php +++ b/Relay/Mutation/PayloadType.php @@ -46,9 +46,6 @@ public function __construct(array $config) ); $name = str_replace('Payload', '', $config['name']); - if (empty($name)) { - $name = $config['name']; - } parent::__construct([ 'name' => $name.'Payload', diff --git a/Request/BatchParser.php b/Request/BatchParser.php new file mode 100644 index 000000000..9aefdf43f --- /dev/null +++ b/Request/BatchParser.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Request; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +class BatchParser implements ParserInterface +{ + /** + * @param Request $request + * @return array + */ + public function parse(Request $request) + { + // Extracts the GraphQL request parameters + $data = $this->getParsedBody($request); + + if (empty($data)) { + throw new BadRequestHttpException('Must provide at least one valid query.'); + } + + foreach($data as $i => &$entry) { + if (empty($entry[static::PARAM_QUERY]) || !is_string($entry[static::PARAM_QUERY])) { + throw new BadRequestHttpException(sprintf('No valid query found in node "%s"', $i)); + } + + $entry = $entry + [ + static::PARAM_VARIABLES => null, + static::PARAM_OPERATION_NAME => null, + ]; + } + + return $data; + } + + /** + * Gets the body from the request. + * + * @param Request $request + * + * @return array + */ + private function getParsedBody(Request $request) + { + $type = explode(';', $request->headers->get('content-type'))[0]; + + // JSON object + if ($type !== static::CONTENT_TYPE_JSON) { + throw new BadRequestHttpException(sprintf('Only request with content type "%" is accepted.', static::CONTENT_TYPE_JSON)); + } + + $parsedBody = json_decode($request->getContent(), true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new BadRequestHttpException('POST body sent invalid JSON'); + } + + return $parsedBody; + } +} diff --git a/Request/Parser.php b/Request/Parser.php index 667ebf901..011216866 100644 --- a/Request/Parser.php +++ b/Request/Parser.php @@ -14,13 +14,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -class Parser +class Parser implements ParserInterface { /** - * Parses the HTTP request and extracts the GraphQL request parameters. - * * @param Request $request - * * @return array */ public function parse(Request $request) @@ -46,23 +43,22 @@ private function getParsedBody(Request $request) switch ($type) { // Plain string - case 'application/graphql': - $parsedBody = ['query' => $body]; + case static::CONTENT_TYPE_GRAPHQL: + $parsedBody = [static::PARAM_QUERY => $body]; break; // JSON object - case 'application/json': - $json = json_decode($body, true); + case static::CONTENT_TYPE_JSON: + $parsedBody = json_decode($body, true); if (JSON_ERROR_NONE !== json_last_error()) { throw new BadRequestHttpException('POST body sent invalid JSON'); } - $parsedBody = $json; break; // URL-encoded query-string - case 'application/x-www-form-urlencoded': - case 'multipart/form-data': + case static::CONTENT_TYPE_FORM: + case static::CONTENT_TYPE_FORM_DATA: $parsedBody = $request->request->all(); break; @@ -86,18 +82,18 @@ private function getParams(Request $request, array $data = []) { // Add default request parameters $data = $data + [ - 'query' => null, - 'variables' => null, - 'operationName' => null, + static::PARAM_QUERY => null, + static::PARAM_VARIABLES => null, + static::PARAM_OPERATION_NAME => null, ]; // Keep a reference to the query-string $qs = $request->query; // Override request using query-string parameters - $query = $qs->has('query') ? $qs->get('query') : $data['query']; - $variables = $qs->has('variables') ? $qs->get('variables') : $data['variables']; - $operationName = $qs->has('operationName') ? $qs->get('operationName') : $data['operationName']; + $query = $qs->has(static::PARAM_QUERY) ? $qs->get(static::PARAM_QUERY) : $data[static::PARAM_QUERY]; + $variables = $qs->has(static::PARAM_VARIABLES) ? $qs->get(static::PARAM_VARIABLES) : $data[static::PARAM_VARIABLES]; + $operationName = $qs->has(static::PARAM_OPERATION_NAME) ? $qs->get(static::PARAM_OPERATION_NAME) : $data[static::PARAM_OPERATION_NAME]; // `query` parameter is mandatory. if (empty($query)) { @@ -115,9 +111,9 @@ private function getParams(Request $request, array $data = []) } return [ - 'query' => $query, - 'variables' => $variables, - 'operationName' => $operationName, + static::PARAM_QUERY => $query, + static::PARAM_VARIABLES => $variables, + static::PARAM_OPERATION_NAME => $operationName, ]; } } diff --git a/Request/ParserInterface.php b/Request/ParserInterface.php new file mode 100644 index 000000000..f4bc49578 --- /dev/null +++ b/Request/ParserInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Request; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +interface ParserInterface +{ + const CONTENT_TYPE_GRAPHQL ='application/graphql'; + const CONTENT_TYPE_JSON = 'application/json'; + const CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded'; + const CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; + + const PARAM_QUERY = 'query'; + const PARAM_VARIABLES = 'variables'; + const PARAM_OPERATION_NAME = 'operationName'; + + /** + * Parses the HTTP request and extracts the GraphQL request parameters. + * + * @param Request $request + * + * @throw BadRequestHttpException + * + * @return array + */ + public function parse(Request $request); +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index e1c876d78..5d860817e 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -40,6 +40,9 @@ services: overblog_graphql.request_parser: class: Overblog\GraphQLBundle\Request\Parser + overblog_graphql.request_batch_parser: + class: Overblog\GraphQLBundle\Request\BatchParser + overblog_graphql.schema: class: GraphQL\Schema public: false @@ -112,6 +115,7 @@ services: public: false arguments: - "@overblog_graphql.cache_expression_language_parser" + - [] calls: - ["setContainer", ["@service_container"]] diff --git a/Tests/Functional/Controller/GraphControllerTest.php b/Tests/Functional/Controller/GraphControllerTest.php index 346c34718..6836b7947 100644 --- a/Tests/Functional/Controller/GraphControllerTest.php +++ b/Tests/Functional/Controller/GraphControllerTest.php @@ -15,6 +15,32 @@ class GraphControllerTest extends TestCase { + private $friendsQuery = << [ 'friends' => [ @@ -41,22 +67,7 @@ public function testEndpointAction() { $client = static::createClient(['test_case' => 'connection']); - $query = <<request('GET', '/', ['query' => $query], [], ['CONTENT_TYPE' => 'application/graphql']); + $client->request('GET', '/', ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/graphql']); $result = $client->getResponse()->getContent(); $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); } @@ -131,32 +142,77 @@ public function testEndpointActionWithOperationName() { $client = static::createClient(['test_case' => 'connection']); - $query = <<friendsQuery . "\n" .$this->friendsTotalCountQuery; $client->request('POST', '/', ['query' => $query, 'operationName' => 'FriendsQuery'], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded']); $result = $client->getResponse()->getContent(); $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); } + + public function testBatchEndpointAction() + { + $client = static::createClient(['test_case' => 'connection']); + + $data = [ + 'friends' => [ + 'query' => $this->friendsQuery, + ], + 'friendsTotalCount' => [ + 'query' => $this->friendsTotalCountQuery, + ], + ]; + + $client->request('POST', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); + $result = $client->getResponse()->getContent(); + + $expected = [ + 'friends' => ['data' => $this->expectedData], + 'friendsTotalCount' => ['data' => ['user' => ['friends' => ['totalCount' => 4]]]], + ]; + $this->assertEquals($expected, json_decode($result, true), $result); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Must provide at least one valid query. + */ + public function testBatchEndpointWithEmptyQuery() + { + $client = static::createClient(); + $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{}'); + $client->getResponse()->getContent(); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Only request with content type " is accepted. + */ + public function testBatchEndpointWrongContentType() + { + $client = static::createClient(); + $client->request('GET', '/?batch'); + $client->getResponse()->getContent(); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage POST body sent invalid JSON + */ + public function testBatchEndpointWithInvalidJson() + { + $client = static::createClient(); + $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{'); + $client->getResponse()->getContent(); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage No valid query found in node "test" + */ + public function testBatchEndpointWithInvalidQuery() + { + $client = static::createClient(); + $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{"test" : {"query": 1}}'); + $client->getResponse()->getContent(); + } } diff --git a/Tests/Functional/app/config/plural/config.yml b/Tests/Functional/app/config/plural/config.yml index 08c8b4930..f89c92cd5 100644 --- a/Tests/Functional/app/config/plural/config.yml +++ b/Tests/Functional/app/config/plural/config.yml @@ -15,5 +15,5 @@ overblog_graphql: mappings: types: - - type: yml + type: xml dir: %kernel.root_dir%/config/plural/mapping diff --git a/Tests/Functional/app/config/plural/mapping/Query.types.xml b/Tests/Functional/app/config/plural/mapping/Query.types.xml new file mode 100644 index 000000000..4d67b9336 --- /dev/null +++ b/Tests/Functional/app/config/plural/mapping/Query.types.xml @@ -0,0 +1,25 @@ + + + + + object + + + + PluralIdentifyingRoot + + usernames + Map from a username to the user + String + User + @=resolver("plural_single_input", [value, info]) + + + + + + + diff --git a/Tests/Functional/app/config/plural/mapping/Query.types.yml b/Tests/Functional/app/config/plural/mapping/Query.types.yml deleted file mode 100644 index 19881f026..000000000 --- a/Tests/Functional/app/config/plural/mapping/Query.types.yml +++ /dev/null @@ -1,12 +0,0 @@ -Query: - type: object - config: - fields: - usernames: - builder: PluralIdentifyingRoot - builderConfig: - argName: 'usernames' - description: 'Map from a username to the user' - inputType: String - outputType: User - resolveSingleInput: '@=resolver("plural_single_input", [value, info])' diff --git a/Tests/Functional/app/config/plural/mapping/User.types.xml b/Tests/Functional/app/config/plural/mapping/User.types.xml new file mode 100644 index 000000000..3d489e60a --- /dev/null +++ b/Tests/Functional/app/config/plural/mapping/User.types.xml @@ -0,0 +1,21 @@ + + + + + object + + + + String + + + String + + + + + + diff --git a/Tests/Functional/app/config/plural/mapping/User.types.yml b/Tests/Functional/app/config/plural/mapping/User.types.yml deleted file mode 100644 index fc46ccc24..000000000 --- a/Tests/Functional/app/config/plural/mapping/User.types.yml +++ /dev/null @@ -1,8 +0,0 @@ -User: - type: object - config: - fields: - username: - type: String - url: - type: String