From a299eed9c88df2f53fd81fe9307c10a4eb3c5b8d Mon Sep 17 00:00:00 2001 From: Daniel Bosen Date: Thu, 4 May 2023 07:23:47 +0200 Subject: [PATCH 1/5] thunder rout entity data provider --- .../DataProducer/ThunderRouteEntity.php | 165 ++++++++++++++++++ .../src/Traits/ResolverHelperTrait.php | 2 +- .../tests/src/Functional/SchemaTest.php | 30 +++- 3 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php new file mode 100644 index 000000000..ff3abfcb3 --- /dev/null +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php @@ -0,0 +1,165 @@ +get('entity_type.manager'), + $container->get('graphql.buffer.entity'), + $container->get('graphql.buffer.entity_revision') + ); + } + + /** + * RouteEntity constructor. + * + * @param array $configuration + * The plugin configuration array. + * @param string $pluginId + * The plugin id. + * @param mixed $pluginDefinition + * The plugin definition array. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The language manager service. + * @param \Drupal\graphql\GraphQL\Buffers\EntityBuffer $entityBuffer + * The entity buffer service. + * @param \Drupal\graphql\GraphQL\Buffers\EntityRevisionBuffer $entityRevisionBuffer + * The entity revision buffer service. + * @codeCoverageIgnore + */ + public function __construct( + array $configuration, + $pluginId, + $pluginDefinition, + EntityTypeManagerInterface $entityTypeManager, + EntityBuffer $entityBuffer, + EntityRevisionBuffer $entityRevisionBuffer + ) { + parent::__construct($configuration, $pluginId, $pluginDefinition); + $this->entityTypeManager = $entityTypeManager; + $this->entityBuffer = $entityBuffer; + $this->entityRevisionBuffer = $entityRevisionBuffer; + } + + /** + * Resolver. + * + * @param \Drupal\Core\Url|mixed $url + * The URL to get the route entity from. + * @param string|null $language + * The language code to get a translation of the entity. + * @param \Drupal\graphql\GraphQL\Execution\FieldContext $context + * The GraphQL field context. + */ + public function resolve($url, ?string $language, FieldContext $context): ?Deferred { + if (!$url instanceof Url) { + return NULL; + } + + [, $type, $subType] = explode('.', $url->getRouteName()); + $parameters = $url->getRouteParameters(); + + if ($subType === 'latest_version') { + $id = $this->entityTypeManager + ->getStorage($type) + ->getLatestRevisionId($parameters[$type]); + $resolver = $this->entityRevisionBuffer->add($type, $id); + } + elseif ($subType === 'revision') { + $resolver = $this->entityRevisionBuffer->add($type, $parameters[$type . '_revision']); + } + else { + $resolver = $this->entityBuffer->add($type, $parameters[$type]); + } + + return new Deferred(function () use ($type, $resolver, $context, $language) { + if (!$entity = $resolver()) { + // If there is no entity with this id, add the list cache tags so that + // the cache entry is purged whenever a new entity of this type is + // saved. + $type = $this->entityTypeManager->getDefinition($type); + /** @var \Drupal\Core\Entity\EntityTypeInterface $type */ + $tags = $type->getListCacheTags(); + $context->addCacheTags($tags)->addCacheTags(['4xx-response']); + return NULL; + } + + // Get the correct translation. + if (isset($language) && $language != $entity->language()->getId() && $entity instanceof TranslatableInterface) { + $entity = $entity->getTranslation($language); + $entity->addCacheContexts(["static:language:{$language}"]); + } + + $access = $entity->access('view', NULL, TRUE); + $context->addCacheableDependency($access); + if ($access->isAllowed()) { + return $entity; + } + return NULL; + }); + } + +} diff --git a/modules/thunder_gqls/src/Traits/ResolverHelperTrait.php b/modules/thunder_gqls/src/Traits/ResolverHelperTrait.php index a3f529d19..26712d2ab 100644 --- a/modules/thunder_gqls/src/Traits/ResolverHelperTrait.php +++ b/modules/thunder_gqls/src/Traits/ResolverHelperTrait.php @@ -111,7 +111,7 @@ public function fromRoute(ResolverInterface $path) { return $this->builder->compose( $this->builder->produce('route_load') ->map('path', $path), - $this->builder->produce('route_entity') + $this->builder->produce('thunder_route_entity') ->map('url', $this->builder->fromParent()) ->map('language', $this->builder->produce('thunder_language') ->map('path', $path) diff --git a/modules/thunder_gqls/tests/src/Functional/SchemaTest.php b/modules/thunder_gqls/tests/src/Functional/SchemaTest.php index 8f17f8c55..c620b4051 100644 --- a/modules/thunder_gqls/tests/src/Functional/SchemaTest.php +++ b/modules/thunder_gqls/tests/src/Functional/SchemaTest.php @@ -39,7 +39,7 @@ public function testSchema(): void { } /** - * Tests the article schema. + * Tests unpublished access. * * @throws \GuzzleHttp\Exception\GuzzleException */ @@ -156,4 +156,32 @@ public function testValidSchema(): void { $this->assertEmpty($validator->getMissingResolvers($server), "The schema 'thunder_graphql' contains types without a resolver."); } + /** + * Test the latest revision query. + */ + public function testLatestRevision(): void { + $node = Node::create([ + 'title' => 'Test node', + 'field_seo_title' => 'SEO title', + 'type' => 'article', + 'status' => Node::NOT_PUBLISHED, + ]); + $node->save(); + + $query = << $node->toUrl()->toString()]; + $response = $this->query($query, Json::encode($variables)); + $this->assertEquals(200, $response->getStatusCode(), 'Response not 200'); + $this->assertEquals(['seoTitle' => 'SEO title'], $this->jsonDecode($response->getBody())['data']['page']); + } } From 6322fce86b32ed51e2f136646fe937b3bbf436ff Mon Sep 17 00:00:00 2001 From: Daniel Bosen Date: Thu, 4 May 2023 08:12:38 +0200 Subject: [PATCH 2/5] ignore static --- .../src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php | 2 -- phpstan-baseline.neon | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php index ff3abfcb3..f46f82d07 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php @@ -59,8 +59,6 @@ class ThunderRouteEntity extends DataProducerPluginBase implements ContainerFact /** * {@inheritdoc} - * - * @codeCoverageIgnore */ public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) { return new static( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b0a637324..36ec2d1e4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -55,6 +55,11 @@ parameters: count: 1 path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRedirect.php + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php + - message: "#^Access to an undefined property Drupal\\\\Core\\\\Entity\\\\ContentEntityInterface\\:\\:\\$field_teaser_media\\.$#" count: 1 From 8d8990d37e9eea40e734b01596b1b1fee39373bb Mon Sep 17 00:00:00 2001 From: Bosen Daniel Date: Thu, 4 May 2023 11:43:07 +0200 Subject: [PATCH 3/5] cbf --- .../src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php | 1 + modules/thunder_gqls/tests/src/Functional/SchemaTest.php | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php index f46f82d07..ab9bcd5be 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php @@ -86,6 +86,7 @@ public static function create(ContainerInterface $container, array $configuratio * The entity buffer service. * @param \Drupal\graphql\GraphQL\Buffers\EntityRevisionBuffer $entityRevisionBuffer * The entity revision buffer service. + * * @codeCoverageIgnore */ public function __construct( diff --git a/modules/thunder_gqls/tests/src/Functional/SchemaTest.php b/modules/thunder_gqls/tests/src/Functional/SchemaTest.php index c620b4051..10bbca75f 100644 --- a/modules/thunder_gqls/tests/src/Functional/SchemaTest.php +++ b/modules/thunder_gqls/tests/src/Functional/SchemaTest.php @@ -177,11 +177,10 @@ public function testLatestRevision(): void { GQL; // Create new unpublished revision. - - $variables = ['path' => $node->toUrl()->toString()]; $response = $this->query($query, Json::encode($variables)); $this->assertEquals(200, $response->getStatusCode(), 'Response not 200'); $this->assertEquals(['seoTitle' => 'SEO title'], $this->jsonDecode($response->getBody())['data']['page']); } + } From 3659fbf560a2f5173ee50458c2f575e79b86a1ee Mon Sep 17 00:00:00 2001 From: Bosen Daniel Date: Thu, 4 May 2023 17:09:07 +0200 Subject: [PATCH 4/5] fix phpstan --- .../Plugin/GraphQL/DataProducer/ThunderRouteEntity.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php index ab9bcd5be..14063abca 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRouteEntity.php @@ -3,6 +3,7 @@ namespace Drupal\thunder_gqls\Plugin\GraphQL\DataProducer; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; @@ -120,11 +121,10 @@ public function resolve($url, ?string $language, FieldContext $context): ?Deferr [, $type, $subType] = explode('.', $url->getRouteName()); $parameters = $url->getRouteParameters(); + $storage = $this->entityTypeManager->getStorage($type); - if ($subType === 'latest_version') { - $id = $this->entityTypeManager - ->getStorage($type) - ->getLatestRevisionId($parameters[$type]); + if ($subType === 'latest_version' && $storage instanceof RevisionableStorageInterface) { + $id = $storage->getLatestRevisionId($parameters[$type]); $resolver = $this->entityRevisionBuffer->add($type, $id); } elseif ($subType === 'revision') { From 311f5a2d3e861d50d7904c32db91a95dc2bd2e87 Mon Sep 17 00:00:00 2001 From: Bosen Daniel Date: Thu, 4 May 2023 17:34:37 +0200 Subject: [PATCH 5/5] add documentation --- docs/developer-guide/headless.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/developer-guide/headless.md b/docs/developer-guide/headless.md index f7083210d..73f589b6f 100644 --- a/docs/developer-guide/headless.md +++ b/docs/developer-guide/headless.md @@ -164,6 +164,33 @@ Then we add the $path variable with a json string like this: This variable can be added in the GraphQL explorer in the corresponding input field. All following examples will assume a variable definition like this. +#### Requesting revisions + +Given a user has the permission to access revisions of content, the $path can contain revision routes as well. +Revision routes are always internal drupal routes. +If /example-page is Node 6 in Drupal, the following variables are valid: + +Get the current revision, which is the default query to the currently published page. +```json +{ + "path": "/example-page" +} +``` + +Get a specific revision, can be an old revision, or a not yet published draft +```json +{ + "path": "/node/6/revision/11/view" +} +``` + +Get the latest revision, which might be an unpublished draft. The latest revision might not be available for a given node. +```json +{ + "path": "/node/6/latest" +} +``` + #### Paragraphs example Articles and taxonomy terms contain paragraph fields in Thunder, the following example shows how to request paragraphs'