Skip to content

Commit 4452ea6

Browse files
authored
feat(dataproducers): Add entity query dataproducers (#1360)
1 parent 9e8f44f commit 4452ea6

3 files changed

Lines changed: 474 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;
4+
5+
use Drupal\graphql\GraphQL\Execution\FieldContext;
6+
7+
/**
8+
* Builds and executes Drupal entity query.
9+
*
10+
* Example for mapping this dataproducer to the schema:
11+
* @code
12+
* $defaultSorting = [
13+
* [
14+
* 'field' => 'created',
15+
* 'direction' => 'DESC',
16+
* ],
17+
* ];
18+
* $registry->addFieldResolver('Query', 'jobApplicationsByUserId',
19+
* $builder->compose(
20+
* $builder->fromArgument('id'),
21+
* $builder->callback(function ($uid) {
22+
* $conditions = [
23+
* [
24+
* 'field' => 'uid',
25+
* 'value' => [$uid],
26+
* ],
27+
* ];
28+
* return $conditions;
29+
* }),
30+
* $builder->produce('entity_query', [
31+
* 'type' => $builder->fromValue('node'),
32+
* 'conditions' => $builder->fromParent(),
33+
* 'offset' => $builder->fromArgument('offset'),
34+
* 'limit' => $builder->fromArgument('limit'),
35+
* 'language' => $builder->fromArgument('language'),
36+
* 'allowed_filters' => $builder->fromValue(['uid']),
37+
* 'bundles' => $builder->fromValue(['job_application']),
38+
* 'sorts' => $builder->fromArgumentWithDefaultValue('sorting', $defaultSorting),
39+
* ]),
40+
* $builder->produce('entity_load_multiple', [
41+
* 'type' => $builder->fromValue('node'),
42+
* 'ids' => $builder->fromParent(),
43+
* ]),
44+
* )
45+
* );
46+
* @endcode
47+
*
48+
* @DataProducer(
49+
* id = "entity_query",
50+
* name = @Translation("Load entities"),
51+
* description = @Translation("Returns entity IDs for a given query"),
52+
* produces = @ContextDefinition("string",
53+
* label = @Translation("Entity IDs"),
54+
* multiple = TRUE
55+
* ),
56+
* consumes = {
57+
* "type" = @ContextDefinition("string",
58+
* label = @Translation("Entity type")
59+
* ),
60+
* "limit" = @ContextDefinition("integer",
61+
* label = @Translation("Limit"),
62+
* required = FALSE,
63+
* default_value = 10
64+
* ),
65+
* "offset" = @ContextDefinition("integer",
66+
* label = @Translation("Offset"),
67+
* required = FALSE,
68+
* default_value = 0
69+
* ),
70+
* "owned_only" = @ContextDefinition("boolean",
71+
* label = @Translation("Query only owned entities"),
72+
* required = FALSE,
73+
* default_value = FALSE
74+
* ),
75+
* "conditions" = @ContextDefinition("any",
76+
* label = @Translation("Conditions"),
77+
* multiple = TRUE,
78+
* required = FALSE,
79+
* default_value = {}
80+
* ),
81+
* "allowed_filters" = @ContextDefinition("string",
82+
* label = @Translation("Allowed filters"),
83+
* multiple = TRUE,
84+
* required = FALSE,
85+
* default_value = {}
86+
* ),
87+
* "languages" = @ContextDefinition("string",
88+
* label = @Translation("Entity languages"),
89+
* multiple = TRUE,
90+
* required = FALSE,
91+
* default_value = {}
92+
* ),
93+
* "bundles" = @ContextDefinition("any",
94+
* label = @Translation("Entity bundles"),
95+
* multiple = TRUE,
96+
* required = FALSE,
97+
* default_value = {}
98+
* ),
99+
* "access" = @ContextDefinition("boolean",
100+
* label = @Translation("Check access"),
101+
* required = FALSE,
102+
* default_value = TRUE
103+
* ),
104+
* "sorts" = @ContextDefinition("any",
105+
* label = @Translation("Sorts"),
106+
* multiple = TRUE,
107+
* default_value = {},
108+
* required = FALSE
109+
* )
110+
* }
111+
* )
112+
*/
113+
class EntityQuery extends EntityQueryBase {
114+
115+
/**
116+
* The default maximum number of items to be capped to prevent DDOS attacks.
117+
*/
118+
const MAX_ITEMS = 100;
119+
120+
/**
121+
* Resolves the entity query.
122+
*
123+
* @param string $type
124+
* Entity type.
125+
* @param int $limit
126+
* Maximum number of queried entities.
127+
* @param int $offset
128+
* Offset to start with.
129+
* @param bool $ownedOnly
130+
* Query only entities owned by current user.
131+
* @param array $conditions
132+
* List of conditions to filter the entities.
133+
* @param array $allowedFilters
134+
* List of fields to be used in conditions to restrict access to data.
135+
* @param string[] $languages
136+
* Languages for queried entities.
137+
* @param string[] $bundles
138+
* List of bundles to be filtered.
139+
* @param bool $access
140+
* Whether entity query should check access.
141+
* @param array $sorts
142+
* List of sorts.
143+
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
144+
* The caching context related to the current field.
145+
*
146+
* @return array
147+
* The list of ids that match this query.
148+
*
149+
* @throws \GraphQL\Error\UserError
150+
* No bundles defined for given entity type.
151+
*/
152+
public function resolve(string $type, int $limit, int $offset, bool $ownedOnly, array $conditions, array $allowedFilters, array $languages, array $bundles, bool $access, array $sorts, FieldContext $context): array {
153+
$query = $this->buildBaseEntityQuery(
154+
$type,
155+
$ownedOnly,
156+
$conditions,
157+
$allowedFilters,
158+
$languages,
159+
$bundles,
160+
$access,
161+
$context
162+
);
163+
164+
// Make sure offset is zero or positive.
165+
$offset = max($offset, 0);
166+
167+
// Make sure limit is positive and cap the max items to prevent DDOS
168+
// attacks.
169+
if ($limit <= 0) {
170+
$limit = 10;
171+
}
172+
$limit = min($limit, self::MAX_ITEMS);
173+
174+
// Apply offset and limit.
175+
$query->range($offset, $limit);
176+
177+
// Add sorts.
178+
foreach ($sorts as $sort) {
179+
if (!empty($sort['field'])) {
180+
if (!empty($sort['direction']) && strtolower($sort['direction']) == 'desc') {
181+
$direction = 'DESC';
182+
}
183+
else {
184+
$direction = 'ASC';
185+
}
186+
$query->sort($sort['field'], $direction);
187+
}
188+
}
189+
190+
$ids = $query->execute();
191+
192+
return $ids;
193+
}
194+
195+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;
4+
5+
use Drupal\Core\Entity\EntityTypeManager;
6+
use Drupal\Core\Entity\Query\QueryInterface;
7+
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
8+
use Drupal\Core\Session\AccountProxyInterface;
9+
use Drupal\graphql\GraphQL\Execution\FieldContext;
10+
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
11+
use GraphQL\Error\UserError;
12+
use Symfony\Component\DependencyInjection\ContainerInterface;
13+
14+
/**
15+
* Base class to share code between entity query and entity query count.
16+
*/
17+
abstract class EntityQueryBase extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
18+
19+
/**
20+
* The entity type manager service.
21+
*
22+
* @var \Drupal\Core\Entity\EntityTypeManager
23+
*/
24+
protected $entityTypeManager;
25+
26+
/**
27+
* The current user proxy.
28+
*
29+
* @var \Drupal\Core\Session\AccountProxyInterface
30+
*/
31+
protected $currentUser;
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
37+
return new static(
38+
$configuration,
39+
$plugin_id,
40+
$plugin_definition,
41+
$container->get('entity_type.manager'),
42+
$container->get('current_user')
43+
);
44+
}
45+
46+
/**
47+
* EntityLoad constructor.
48+
*
49+
* @param array $configuration
50+
* The plugin configuration array.
51+
* @param string $pluginId
52+
* The plugin id.
53+
* @param array $pluginDefinition
54+
* The plugin definition array.
55+
* @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
56+
* The entity type manager service.
57+
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
58+
* The current user proxy.
59+
*/
60+
public function __construct(
61+
array $configuration,
62+
string $pluginId,
63+
array $pluginDefinition,
64+
EntityTypeManager $entityTypeManager,
65+
AccountProxyInterface $current_user
66+
) {
67+
parent::__construct($configuration, $pluginId, $pluginDefinition);
68+
$this->entityTypeManager = $entityTypeManager;
69+
$this->currentUser = $current_user;
70+
}
71+
72+
/**
73+
* Build base entity query which may be reused for count query as well.
74+
*
75+
* @param string $type
76+
* Entity type.
77+
* @param bool $ownedOnly
78+
* Query only entities owned by current user.
79+
* @param array $conditions
80+
* List of conditions to filter the entities.
81+
* @param array $allowedFilters
82+
* List of fields to be used in conditions to restrict access to data.
83+
* @param string[] $languages
84+
* Languages for queried entities.
85+
* @param string[] $bundles
86+
* List of bundles to be filtered.
87+
* @param bool $access
88+
* Whether entity query should check access.
89+
* @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
90+
* The caching context related to the current field.
91+
*
92+
* @return \Drupal\Core\Entity\Query\QueryInterface
93+
* Base entity query.
94+
*
95+
* @throws \GraphQL\Error\UserError
96+
* No bundles defined for given entity type.
97+
*/
98+
protected function buildBaseEntityQuery(string $type, bool $ownedOnly, array $conditions, array $allowedFilters, array $languages, array $bundles, bool $access, FieldContext $context): QueryInterface {
99+
$entity_type = $this->entityTypeManager->getStorage($type);
100+
$query = $entity_type->getQuery();
101+
102+
// Query only those entities which are owned by current user, if desired.
103+
if ($ownedOnly) {
104+
$query->condition('uid', $this->currentUser->id());
105+
// Add user cacheable dependencies.
106+
$account = $this->currentUser->getAccount();
107+
$context->addCacheableDependency($account);
108+
// Cache response per user to make sure the user related result is shown.
109+
$context->addCacheContexts(['user']);
110+
}
111+
112+
// Ensure that desired access checking is performed on the query.
113+
$query->accessCheck($access);
114+
115+
// Filter entities only of given bundles, if desired.
116+
if ($bundles) {
117+
$bundle_key = $entity_type->getEntityType()->getKey('bundle');
118+
if (!$bundle_key) {
119+
throw new UserError('No bundles defined for given entity type.');
120+
}
121+
$query->condition($bundle_key, $bundles, 'IN');
122+
}
123+
124+
// Filter entities by given languages, if desired.
125+
if ($languages) {
126+
$query->condition('langcode', $languages, 'IN');
127+
}
128+
129+
// Filter by given conditions.
130+
foreach ($conditions as $condition) {
131+
if (!in_array($condition['field'], $allowedFilters)) {
132+
throw new UserError("Field '{$condition['field']}' is not allowed as filter.");
133+
}
134+
$operation = $condition['operator'] ?? NULL;
135+
$query->condition($condition['field'], $condition['value'], $operation);
136+
}
137+
138+
return $query;
139+
}
140+
141+
}

0 commit comments

Comments
 (0)