From 5a02c2bd478aaf5ecf29ba09e403d136a381aac2 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Tue, 27 Sep 2016 17:45:30 +0200 Subject: [PATCH] Enable multiple schema --- Controller/GraphController.php | 26 +++++---- Controller/GraphiQLController.php | 10 +++- DependencyInjection/Configuration.php | 29 +++++++--- .../OverblogGraphQLExtension.php | 23 +++++--- README.md | 32 +++++++++++ Request/Executor.php | 45 ++++++++++++++-- Resources/config/routing/graphiql.yml | 5 ++ Resources/config/routing/graphql.yml | 12 +++++ Resources/config/services.yml | 10 ---- .../Controller/GraphControllerTest.php | 54 ++++++++++++++++--- .../Controller/GraphiQLControllerTest.php | 15 +++++- Tests/Request/ExecutorTest.php | 26 +++++++++ 12 files changed, 234 insertions(+), 53 deletions(-) create mode 100644 Tests/Request/ExecutorTest.php diff --git a/Controller/GraphController.php b/Controller/GraphController.php index 96ffe6aff..950946a90 100644 --- a/Controller/GraphController.php +++ b/Controller/GraphController.php @@ -17,40 +17,44 @@ class GraphController extends Controller { - public function endpointAction(Request $request) + public function endpointAction(Request $request, $schemaName = null) { - $payload = $this->processNormalQuery($request); + $payload = $this->processNormalQuery($request, $schemaName); return new JsonResponse($payload, 200); } - public function batchEndpointAction(Request $request) + public function batchEndpointAction(Request $request, $schemaName = null) { - $payloads = $this->processBatchQuery($request); + $payloads = $this->processBatchQuery($request, $schemaName); return new JsonResponse($payloads, 200); } - private function processBatchQuery(Request $request) + private function processBatchQuery(Request $request, $schemaName = null) { $queries = $this->get('overblog_graphql.request_batch_parser')->parse($request); $payloads = []; foreach ($queries as $query) { - $payloadResult = $this->get('overblog_graphql.request_executor')->execute([ - 'query' => $query['query'], - 'variables' => $query['variables'], - ]); + $payloadResult = $this->get('overblog_graphql.request_executor')->execute( + [ + 'query' => $query['query'], + 'variables' => $query['variables'], + ], + [], + $schemaName + ); $payloads[] = ['id' => $query['id'], 'payload' => $payloadResult->toArray()]; } return $payloads; } - private function processNormalQuery(Request $request) + private function processNormalQuery(Request $request, $schemaName = null) { $params = $this->get('overblog_graphql.request_parser')->parse($request); - $data = $this->get('overblog_graphql.request_executor')->execute($params)->toArray(); + $data = $this->get('overblog_graphql.request_executor')->execute($params, [], $schemaName)->toArray(); return $data; } diff --git a/Controller/GraphiQLController.php b/Controller/GraphiQLController.php index d16c5fe1a..194be1d8e 100644 --- a/Controller/GraphiQLController.php +++ b/Controller/GraphiQLController.php @@ -15,12 +15,18 @@ class GraphiQLController extends Controller { - public function indexAction() + public function indexAction($schemaName = null) { + if (null === $schemaName) { + $endpoint = $this->generateUrl('overblog_graphql_endpoint'); + } else { + $endpoint = $this->generateUrl('overblog_graphql_multiple_endpoint', ['schemaName' => $schemaName]); + } + return $this->render( $this->getParameter('overblog_graphql.graphiql_template'), [ - 'endpoint' => $this->generateUrl('overblog_graphql_endpoint'), + 'endpoint' => $endpoint, 'versions' => [ 'graphiql' => $this->getParameter('overblog_graphql.versions.graphiql'), 'react' => $this->getParameter('overblog_graphql.versions.react'), diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 3f61b0813..e61942751 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -46,11 +46,26 @@ public function getConfigTreeBuilder() ->scalarNode('internal_error_message')->defaultNull()->end() ->booleanNode('config_validation')->defaultValue($this->debug)->end() ->arrayNode('schema') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('query')->defaultNull()->end() - ->scalarNode('mutation')->defaultNull()->end() - ->scalarNode('subscription')->defaultNull()->end() + ->beforeNormalization() + ->ifTrue(function ($v) { + $needNormalization = isset($v['query']) && is_string($v['query']) || + isset($v['mutation']) && is_string($v['mutation']) || + isset($v['subscription']) && is_string($v['subscription']); + + return $needNormalization; + }) + ->then(function ($v) { + return ['default' => $v]; + }) + ->end() + ->useAttributeAsKey('name') + ->prototype('array') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('query')->defaultNull()->end() + ->scalarNode('mutation')->defaultNull()->end() + ->scalarNode('subscription')->defaultNull()->end() + ->end() ->end() ->end() ->arrayNode('mappings') @@ -142,8 +157,8 @@ public function getConfigTreeBuilder() ->arrayNode('versions') ->addDefaultsIfNotSet() ->children() - ->scalarNode('graphiql')->defaultValue('0.7.1')->end() - ->scalarNode('react')->defaultValue('15.0.2')->end() + ->scalarNode('graphiql')->defaultValue('0.7.8')->end() + ->scalarNode('react')->defaultValue('15.3.2')->end() ->end() ->end() ->end(); diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php index 90f04f637..5eb774e44 100644 --- a/DependencyInjection/OverblogGraphQLExtension.php +++ b/DependencyInjection/OverblogGraphQLExtension.php @@ -14,8 +14,10 @@ use Overblog\GraphQLBundle\Config\TypeWithOutputFieldsDefinition; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class OverblogGraphQLExtension extends Extension implements PrependExtensionInterface @@ -120,13 +122,18 @@ private function setSchemaBuilderArguments(array $config, ContainerBuilder $cont private function setSchemaArguments(array $config, ContainerBuilder $container) { if (isset($config['definitions']['schema'])) { - $container - ->getDefinition($this->getAlias().'.schema') - ->replaceArgument(0, $config['definitions']['schema']['query']) - ->replaceArgument(1, $config['definitions']['schema']['mutation']) - ->replaceArgument(2, $config['definitions']['schema']['subscription']) - ->setPublic(true) - ; + $executorDefinition = $container->getDefinition($this->getAlias().'.request_executor'); + + foreach ($config['definitions']['schema'] as $schemaName => $schemaConfig) { + $schemaID = sprintf('%s.schema_%s', $this->getAlias(), $schemaName); + $definition = new Definition('GraphQL\Schema'); + $definition->setFactory([new Reference('overblog_graphql.schema_builder'), 'create']); + $definition->setArguments([$schemaConfig['query'], $schemaConfig['mutation'], $schemaConfig['subscription']]); + $definition->setPublic(false); + $container->setDefinition($schemaID, $definition); + + $executorDefinition->addMethodCall('addSchema', [$schemaName, new Reference($schemaID)]); + } } } @@ -153,7 +160,7 @@ public function getConfiguration(array $config, ContainerBuilder $container) /** * Returns a list of custom exceptions mapped to error/warning classes. * - * @param array $config + * @param array $exceptionConfig * @return array Custom exception map, [exception => UserError/UserWarning]. */ private function buildExceptionMap(array $exceptionConfig) diff --git a/README.md b/README.md index f7cb609a7..7ce39dbb8 100644 --- a/README.md +++ b/README.md @@ -903,6 +903,38 @@ Batching Batching can help decrease io between server and client. The default route of batching is `/batch`. +Multiple schema endpoint +------------------------ + +```yaml +#app/config/config.yml + +overblog_graphql: + definitions: + schema: + foo: + query: fooQuery + bar: + query: barQuery + mutation: barMutation +``` + +**foo** schema endpoint can be access: + +type | Path +-----| ----- +simple request | `/graphql/foo` +batch request | `/graphql/foo/batch` +graphiQL | `/graphiql/foo` + +**bar** schema endpoint can be access: + +type | Path +-----| ----- +simple request | `/graphql/bar` +batch request | `/graphql/bar/batch` +graphiQL | `/graphiql/bar` + Contribute ---------- diff --git a/Request/Executor.php b/Request/Executor.php index caee39642..559737cc2 100644 --- a/Request/Executor.php +++ b/Request/Executor.php @@ -20,10 +20,14 @@ use Overblog\GraphQLBundle\Event\Events; use Overblog\GraphQLBundle\Event\ExecutorContextEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class Executor { - private $schema; + /** + * @var Schema[] + */ + private $schemas; /** * @var EventDispatcherInterface|null @@ -36,14 +40,20 @@ class Executor /** @var ErrorHandler|null */ private $errorHandler; - public function __construct(Schema $schema, EventDispatcherInterface $dispatcher = null, $throwException = false, ErrorHandler $errorHandler = null) + public function __construct(EventDispatcherInterface $dispatcher = null, $throwException = false, ErrorHandler $errorHandler = null) { - $this->schema = $schema; $this->dispatcher = $dispatcher; $this->throwException = (bool) $throwException; $this->errorHandler = $errorHandler; } + public function addSchema($name, Schema $schema) + { + $this->schemas[$name] = $schema; + + return $this; + } + public function setMaxQueryDepth($maxQueryDepth) { /** @var QueryDepth $queryDepth */ @@ -70,7 +80,7 @@ public function setThrowException($throwException) return $this; } - public function execute(array $data, array $context = []) + public function execute(array $data, array $context = [], $schemaName = null) { if (null !== $this->dispatcher) { $event = new ExecutorContextEvent($context); @@ -78,8 +88,10 @@ public function execute(array $data, array $context = []) $context = $event->getExecutorContext(); } + $schema = $this->getSchema($schemaName); + $executionResult = GraphQL::executeAndReturnResult( - $this->schema, + $schema, isset($data[ParserInterface::PARAM_QUERY]) ? $data[ParserInterface::PARAM_QUERY] : null, $context, $context, @@ -93,4 +105,27 @@ public function execute(array $data, array $context = []) return $executionResult; } + + /** + * @param string|null $name + * + * @return Schema + */ + public function getSchema($name = null) + { + if (empty($this->schemas)) { + throw new \RuntimeException('At least one schema should be declare.'); + } + + if (null === $name) { + $schema = array_values($this->schemas)[0]; + } else { + if (!isset($this->schemas[$name])) { + throw new NotFoundHttpException(sprintf('Could not found "%s" schema.', $name)); + } + $schema = $this->schemas[$name]; + } + + return $schema; + } } diff --git a/Resources/config/routing/graphiql.yml b/Resources/config/routing/graphiql.yml index 5a4288dcc..2031999a5 100644 --- a/Resources/config/routing/graphiql.yml +++ b/Resources/config/routing/graphiql.yml @@ -2,3 +2,8 @@ overblog_graphql_graphiql: path: /graphiql defaults: _controller: OverblogGraphQLBundle:GraphiQL:index + +overblog_graphql_graphiql_multiple: + path: /graphiql/{schemaName} + defaults: + _controller: OverblogGraphQLBundle:GraphiQL:index diff --git a/Resources/config/routing/graphql.yml b/Resources/config/routing/graphql.yml index 1e9dbd8f6..c3b91a12f 100644 --- a/Resources/config/routing/graphql.yml +++ b/Resources/config/routing/graphql.yml @@ -9,3 +9,15 @@ overblog_graphql_batch_endpoint: defaults: _controller: OverblogGraphQLBundle:Graph:batchEndpoint _format: "json" + +overblog_graphql_multiple_endpoint: + path: /graphql/{schemaName} + defaults: + _controller: OverblogGraphQLBundle:Graph:endpoint + _format: "json" + +overblog_graphql_batch_multiple_endpoint: + path: /graphql/{schemaName}/batch + defaults: + _controller: OverblogGraphQLBundle:Graph:batchEndpoint + _format: "json" diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 4c6152c15..34d766bdd 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -14,7 +14,6 @@ services: overblog_graphql.request_executor: class: Overblog\GraphQLBundle\Request\Executor arguments: - - "@overblog_graphql.schema" - "@event_dispatcher" - "%kernel.debug%" - "@overblog_graphql.error_handler" @@ -28,15 +27,6 @@ services: overblog_graphql.request_batch_parser: class: Overblog\GraphQLBundle\Request\BatchParser - overblog_graphql.schema: - class: GraphQL\Schema - public: false - factory: ["@overblog_graphql.schema_builder", create] - arguments: - - ~ - - ~ - - ~ - overblog_graphql.schema_builder: class: Overblog\GraphQLBundle\Definition\Builder\SchemaBuilder public: false diff --git a/Tests/Functional/Controller/GraphControllerTest.php b/Tests/Functional/Controller/GraphControllerTest.php index a16afb901..19fb7d9ac 100644 --- a/Tests/Functional/Controller/GraphControllerTest.php +++ b/Tests/Functional/Controller/GraphControllerTest.php @@ -63,15 +63,28 @@ class GraphControllerTest extends TestCase ], ]; - public function testEndpointAction() + /** + * @param $uri + * @dataProvider graphQLEndpointUriProvider + */ + public function testEndpointAction($uri) { $client = static::createClient(['test_case' => 'connection']); - $client->request('GET', '/', ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/graphql']); + $client->request('GET', $uri, ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/graphql']); $result = $client->getResponse()->getContent(); $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); } + + public function graphQLEndpointUriProvider() + { + return [ + ['/'], + ['/graphql/default'], + ]; + } + /** * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * @expectedExceptionMessage Must provide query parameter @@ -115,8 +128,6 @@ public function testEndpointActionWithVariables() EOF; $client->request('GET', '/', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode(['query' => $query, 'variables' => '{"firstFriends": 2}'])); - $result = $client->getResponse()->getContent(); - $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); } /** @@ -134,8 +145,23 @@ public function testEndpointActionWithInvalidVariables() EOF; $client->request('GET', '/', ['query' => $query, 'variables' => '"firstFriends": 2}']); - $result = $client->getResponse()->getContent(); - $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * @expectedExceptionMessage Could not found "fake" schema. + */ + public function testMultipleEndpointActionWithUnknownSchemaName() + { + $client = static::createClient(['test_case' => 'connection']); + + $query = <<request('GET', '/graphql/fake', ['query' => $query]); } public function testEndpointActionWithOperationName() @@ -149,7 +175,11 @@ public function testEndpointActionWithOperationName() $this->assertEquals(['data' => $this->expectedData], json_decode($result, true), $result); } - public function testBatchEndpointAction() + /** + * @param $uri + * @dataProvider graphQLBatchEndpointUriProvider + */ + public function testBatchEndpointAction($uri) { $client = static::createClient(['test_case' => 'connection']); @@ -164,7 +194,7 @@ public function testBatchEndpointAction() ], ]; - $client->request('POST', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); + $client->request('POST', $uri, [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); $result = $client->getResponse()->getContent(); $expected = [ @@ -174,6 +204,14 @@ public function testBatchEndpointAction() $this->assertEquals($expected, json_decode($result, true), $result); } + public function graphQLBatchEndpointUriProvider() + { + return [ + ['/batch'], + ['/graphql/default/batch'], + ]; + } + /** * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * @expectedExceptionMessage Must provide at least one valid query. diff --git a/Tests/Functional/Controller/GraphiQLControllerTest.php b/Tests/Functional/Controller/GraphiQLControllerTest.php index 439fdf709..907af5f5c 100644 --- a/Tests/Functional/Controller/GraphiQLControllerTest.php +++ b/Tests/Functional/Controller/GraphiQLControllerTest.php @@ -15,11 +15,22 @@ class GraphiQLControllerTest extends TestCase { - public function testIndexAction() + /** + * @dataProvider graphiQLUriProvider + */ + public function testIndexAction($uri) { $client = static::createClient(); - $client->request('GET', '/graphiql'); + $client->request('GET', $uri); $this->assertEquals(200, $client->getResponse()->getStatusCode()); } + + public function graphiQLUriProvider() + { + return [ + ['/graphiql'], + ['/graphiql/default'] + ]; + } } diff --git a/Tests/Request/ExecutorTest.php b/Tests/Request/ExecutorTest.php new file mode 100644 index 000000000..6797d6b55 --- /dev/null +++ b/Tests/Request/ExecutorTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Resolver; + +use Overblog\GraphQLBundle\Request\Executor; + +class ExecutorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage At least one schema should be declare. + */ + public function testGetSchemaNoSchemaFound() + { + (new Executor())->getSchema('default'); + } +}