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

thunder rout entity data provider #707

Draft
wants to merge 7 commits into
base: 6.5.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/developer-guide/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[markdownlint] reported by reviewdog 🐶
MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```json"]

{
"path": "/example-page"
}
```

Get a specific revision, can be an old revision, or a not yet published draft
```json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[markdownlint] reported by reviewdog 🐶
MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[markdownlint] reported by reviewdog 🐶
MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```json"]

{
"path": "/node/6/latest"
}
```

#### Paragraphs example

Articles and taxonomy terms contain paragraph fields in Thunder, the following example shows how to request paragraphs'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

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;
use Drupal\graphql\GraphQL\Buffers\EntityBuffer;
use Drupal\graphql\GraphQL\Buffers\EntityRevisionBuffer;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use GraphQL\Deferred;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Loads the entity associated with the current URL.
*
* @DataProducer(
* id = "thunder_route_entity",
* name = @Translation("Load entity by uuid"),
* description = @Translation("The entity belonging to the current url."),
* produces = @ContextDefinition("entity",
* label = @Translation("Entity")
* ),
* consumes = {
* "url" = @ContextDefinition("any",
* label = @Translation("The URL")
* ),
* "language" = @ContextDefinition("string",
* label = @Translation("Language"),
* required = FALSE
* )
* }
* )
*/
class ThunderRouteEntity extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;

/**
* The entity buffer service.
*
* @var \Drupal\graphql\GraphQL\Buffers\EntityBuffer
*/
protected $entityBuffer;

/**
* The entity buffer service.
*
* @var \Drupal\graphql\GraphQL\Buffers\EntityRevisionBuffer
*/
protected $entityRevisionBuffer;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
return new static(
$configuration,
$pluginId,
$pluginDefinition,
$container->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();
$storage = $this->entityTypeManager->getStorage($type);

if ($subType === 'latest_version' && $storage instanceof RevisionableStorageInterface) {
$id = $storage->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;
});
}

}
2 changes: 1 addition & 1 deletion modules/thunder_gqls/src/Traits/ResolverHelperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion modules/thunder_gqls/tests/src/Functional/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function testSchema(): void {
}

/**
* Tests the article schema.
* Tests unpublished access.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
Expand Down Expand Up @@ -156,4 +156,31 @@ 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 = <<<GQL
query (\$path: String!) {
page(path: \$path) {
seoTitle
}
}
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']);
}

}
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down