Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add APQ plugin #1189

Merged
merged 13 commits into from
Sep 28, 2022
15 changes: 15 additions & 0 deletions graphql.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ services:
tags:
- { name: cache.context }

# Cache bin for the persisted queries.
cache.graphql.apq:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: cache_factory:get
arguments: [graphql_apq]

# Cache bin for the parsed sdl ast.
cache.graphql.ast:
class: Drupal\Core\Cache\CacheBackendInterface
Expand Down Expand Up @@ -108,6 +116,13 @@ services:
tags:
- { name: event_subscriber }

# Cache the queries to be persistent.
graphql.apq_subscriber:
class: Drupal\graphql\EventSubscriber\ApqSubscriber
arguments: ['@cache.graphql.apq', '@request_stack']
tags:
- { name: event_subscriber }

# Plugin manager for schemas
plugin.manager.graphql.schema:
class: Drupal\graphql\Plugin\SchemaPluginManager
Expand Down
70 changes: 70 additions & 0 deletions src/EventSubscriber/ApqSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Drupal\graphql\EventSubscriber;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\graphql\Event\OperationEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Save persisted queries to cache.
*/
class ApqSubscriber implements EventSubscriberInterface {

/**
* The cache to store persisted queries.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;

/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request|null
*/
protected $request;

/**
* Constructs a ApqSubscriber object.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache to store persisted queries.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
*/
public function __construct(CacheBackendInterface $cache, RequestStack $requestStack) {
$this->cache = $cache;
$this->request = $requestStack->getCurrentRequest();
}

/**
* Handle operation start events.
*
* @param \Drupal\graphql\Event\OperationEvent $event
* The kernel event object.
*/
public function onBeforeOperation(OperationEvent $event): void {
try {
$json = Json::decode($this->request->getContent());
if (!empty($json['extensions']['persistedQuery']['sha256Hash']) && !empty($json['query'])) {
$this->cache->set($json['extensions']['persistedQuery']['sha256Hash'], $json['query']);
}

}
catch (\Exception $exception) {
}
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
OperationEvent::GRAPHQL_OPERATION_BEFORE => 'onBeforeOperation',
];
}

}
56 changes: 56 additions & 0 deletions src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\PersistedQuery;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\PersistedQuery\PersistedQueryPluginBase;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Load persisted queries from the cache.
*
* @PersistedQuery(
* id = "automatic_persisted_query",
* label = "Automatic Persisted Query",
* description = "Load persisted queries from the cache."
* )
*/
class AutomaticPersistedQuery extends PersistedQueryPluginBase implements ContainerFactoryPluginInterface {

/**
* The cache to store persisted queries.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;

/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache) {
parent::__construct($configuration, $plugin_id, $plugin_definition);

$this->cache = $cache;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('cache.graphql.apq'));
}

/**
* {@inheritdoc}
*/
public function getQuery($id, OperationParams $operation) {
if ($query = $this->cache->get($id)) {
return $query->data;
}
throw new RequestError('PersistedQueryNotFound');
}

}
43 changes: 43 additions & 0 deletions tests/src/Kernel/Framework/PersistedQueriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\Tests\graphql\Kernel\Framework;

use Drupal\Tests\graphql\Kernel\GraphQLTestBase;
use Symfony\Component\HttpFoundation\Request;

/**
* Tests the entire query result pipeline when using persisted queries.
Expand Down Expand Up @@ -51,6 +52,7 @@ protected function setUp(): void {
$this->plugin_one = $manager->createInstance('persisted_query_plugin_one');
$this->plugin_two = $manager->createInstance('persisted_query_plugin_two');
$this->plugin_three = $manager->createInstance('persisted_query_plugin_three');
$this->plugin_apq = $manager->createInstance('automatic_persisted_query');
}

/**
Expand Down Expand Up @@ -115,4 +117,45 @@ public function persistedQueriesDataProvider(): array {
];
}

/**
* Test the automatic persisted queries plugin.
*/
public function testAutomaticPersistedQueries(): void {
// Before adding the persisted query plugins to the server, we want to make
// sure that there are no existing plugins already there.
$this->server->removeAllPersistedQueryInstances();
$this->server->addPersistedQueryInstance($this->plugin_apq);
$this->server->save();

$endpoint = $this->server->get('endpoint');

$parameters['extensions']['persistedQuery']['sha256Hash'] = 'hash';

// Check we get PersistedQueryNotFound.
$request = Request::create($endpoint, 'GET', $parameters);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame([
'errors' => [
[
'message' => 'PersistedQueryNotFound',
'extensions' => ['category' => 'request'],
],
],
], json_decode($result->getContent(), TRUE));

// Post query to endpoint.
$content = json_encode(['query' => 'query { field_one } '] + $parameters);
$request = Request::create($endpoint, 'POST', [], [], [], [], $content);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE));

// Execute first request again.
$request = Request::create($endpoint, 'GET', $parameters);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE));
}

}