Skip to content

Commit

Permalink
feat(entity-query-count): Introducing entity query count data producer (
Browse files Browse the repository at this point in the history
  • Loading branch information
rthideaway authored Nov 25, 2020
2 parents cfae4df + 5de778d commit 5202e0f
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 105 deletions.
143 changes: 38 additions & 105 deletions src/Plugin/GraphQL/DataProducer/Entity/EntityQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;

use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use GraphQL\Error\UserError;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Builds and executes Drupal entity query.
Expand Down Expand Up @@ -52,15 +46,17 @@
* required = FALSE,
* default_value = {}
* ),
* "language" = @ContextDefinition("string",
* label = @Translation("Entity languages(s)"),
* "languages" = @ContextDefinition("string",
* label = @Translation("Entity languages"),
* multiple = TRUE,
* required = FALSE
* required = FALSE,
* default_value = {}
* ),
* "bundles" = @ContextDefinition("any",
* label = @Translation("Entity bundle(s)"),
* label = @Translation("Entity bundles"),
* multiple = TRUE,
* required = FALSE
* required = FALSE,
* default_value = {}
* ),
* "sorts" = @ContextDefinition("any",
* label = @Translation("Sorts"),
Expand All @@ -71,60 +67,12 @@
* }
* )
*/
class EntityQuery extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

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

/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
class EntityQuery extends EntityQueryBase {

/**
* {@inheritdoc}
* The default maximum number of items to be capped to prevent DDOS attacks.
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('current_user')
);
}

/**
* EntityLoad constructor.
*
* @param array $configuration
* The plugin configuration array.
* @param string $pluginId
* The plugin id.
* @param array $pluginDefinition
* The plugin definition array.
* @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(
array $configuration,
string $pluginId,
array $pluginDefinition,
EntityTypeManager $entityTypeManager,
AccountInterface $current_user
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
$this->entityTypeManager = $entityTypeManager;
$this->currentUser = $current_user;
}
const MAX_ITEMS = 100;

/**
* Resolves the entity query.
Expand All @@ -133,19 +81,19 @@ public function __construct(
* Entity type.
* @param int $limit
* Maximum number of queried entities.
* @param int|null $offset
* @param int $offset
* Offset to start with.
* @param bool|null $ownedOnly
* @param bool $ownedOnly
* Query only entities owned by current user.
* @param array|null $conditions
* @param array $conditions
* List of conditions to filter the entities.
* @param array|null $allowedFilters
* @param array $allowedFilters
* List of fields to be used in conditions to restrict access to data.
* @param string|null $language
* Language of queried entities.
* @param array|null $bundles
* @param string[] $languages
* Languages for queried entities.
* @param string[] $bundles
* List of bundles to be filtered.
* @param array|null $sorts
* @param array $sorts
* List of sorts.
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
* The caching context related to the current field.
Expand All @@ -156,46 +104,31 @@ public function __construct(
* @throws \GraphQL\Error\UserError
* No bundles defined for given entity type.
*/
public function resolve(string $type, int $limit = 10, ?int $offset, ?bool $ownedOnly, ?array $conditions, ?array $allowedFilters, ?string $language, ?array $bundles, ?array $sorts, FieldContext $context): array {
// Make sure offset is zero or positive.
$offset = max($offset ?: 0, 0);

$entity_type = $this->entityTypeManager->getStorage($type);
$query = $entity_type->getQuery()
->range($offset, $limit);

// Query only those entities which are owned by current user, if desired.
if ($ownedOnly) {
$query->condition('uid', $this->currentUser->id());
// Add user cacheable dependencies.
$account = $this->currentUser->getAccount();
$context->addCacheableDependency($account);
// Cache response per user to make sure the user related result is shown.
$context->addCacheContexts(['user']);
}
public function resolve(string $type, int $limit, int $offset, bool $ownedOnly, array $conditions, array $allowedFilters, array $languages, array $bundles, array $sorts, FieldContext $context): array {
$query = $this->buildBaseEntityQuery(
$type,
$ownedOnly,
$conditions,
$allowedFilters,
$languages,
$bundles,
$context
);

// Ensure that access checking is performed on the query.
$query->accessCheck(TRUE);
// Make sure offset is zero or positive.
$offset = max($offset, 0);

if (isset($bundles)) {
$bundle_key = $entity_type->getEntityType()->getKey('bundle');
if (!$bundle_key) {
throw new UserError('No bundles defined for given entity type.');
}
$query->condition($bundle_key, $bundles, "IN");
}
if (isset($language)) {
$query->condition('langcode', $language);
// Make sure limit is positive and cap the max items to prevent DDOS
// attacks.
if ($limit <= 0) {
$limit = 10;
}
$limit = min($limit, self::MAX_ITEMS);

foreach ($conditions as $condition) {
if (!in_array($condition['field'], $allowedFilters)) {
throw new UserError("Field '{$condition['field']}' is not allowed as filter.");
}
$operation = isset($condition['operator']) ? $condition['operator'] : NULL;
$query->condition($condition['field'], $condition['value'], $operation);
}
// Apply offset and limit.
$query->range($offset, $limit);

// Add sorts.
foreach ($sorts as $sort) {
if (!empty($sort['field'])) {
if (!empty($sort['direction']) && strtolower($sort['direction']) == 'desc') {
Expand Down
139 changes: 139 additions & 0 deletions src/Plugin/GraphQL/DataProducer/Entity/EntityQueryBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;

use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use GraphQL\Error\UserError;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Base class to share code between entity query and entity query count.
*/
abstract class EntityQueryBase extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

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

/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;

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

/**
* EntityLoad constructor.
*
* @param array $configuration
* The plugin configuration array.
* @param string $pluginId
* The plugin id.
* @param array $pluginDefinition
* The plugin definition array.
* @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(
array $configuration,
string $pluginId,
array $pluginDefinition,
EntityTypeManager $entityTypeManager,
AccountInterface $current_user
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
$this->entityTypeManager = $entityTypeManager;
$this->currentUser = $current_user;
}

/**
* Build base entity query which may be reused for count query as well.
*
* @param string $type
* Entity type.
* @param bool $ownedOnly
* Query only entities owned by current user.
* @param array $conditions
* List of conditions to filter the entities.
* @param array $allowedFilters
* List of fields to be used in conditions to restrict access to data.
* @param string[] $languages
* Languages for queried entities.
* @param string[] $bundles
* List of bundles to be filtered.
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
* The caching context related to the current field.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* Base entity query.
*
* @throws \GraphQL\Error\UserError
* No bundles defined for given entity type.
*/
protected function buildBaseEntityQuery(string $type, bool $ownedOnly, array $conditions, array $allowedFilters, array $languages, array $bundles, FieldContext $context): QueryInterface {
$entity_type = $this->entityTypeManager->getStorage($type);
$query = $entity_type->getQuery();

// Query only those entities which are owned by current user, if desired.
if ($ownedOnly) {
$query->condition('uid', $this->currentUser->id());
// Add user cacheable dependencies.
$account = $this->currentUser->getAccount();
$context->addCacheableDependency($account);
// Cache response per user to make sure the user related result is shown.
$context->addCacheContexts(['user']);
}

// Ensure that access checking is performed on the query.
$query->accessCheck(TRUE);

// Filter entities only of given bundles, if desired.
if ($bundles) {
$bundle_key = $entity_type->getEntityType()->getKey('bundle');
if (!$bundle_key) {
throw new UserError('No bundles defined for given entity type.');
}
$query->condition($bundle_key, $bundles, 'IN');
}

// Filter entities by given languages, if desired.
if ($languages) {
$query->condition('langcode', $languages, 'IN');
}

// Filter by given conditions.
foreach ($conditions as $condition) {
if (!in_array($condition['field'], $allowedFilters)) {
throw new UserError("Field '{$condition['field']}' is not allowed as filter.");
}
$operation = isset($condition['operator']) ? $condition['operator'] : NULL;
$query->condition($condition['field'], $condition['value'], $operation);
}

return $query;
}

}
Loading

0 comments on commit 5202e0f

Please sign in to comment.