1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\graphql\GraphQL\Execution; |
4
|
|
|
|
5
|
|
|
use Drupal\Core\Cache\CacheableMetadata; |
6
|
|
|
use Drupal\Core\Session\AccountProxyInterface; |
7
|
|
|
use Drupal\graphql\GraphQL\Visitors\CacheMetadataCollector; |
8
|
|
|
use Drupal\graphql\Plugin\SchemaPluginManager; |
9
|
|
|
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface; |
10
|
|
|
use GraphQL\Error\Error; |
11
|
|
|
use GraphQL\Error\FormattedError; |
12
|
|
|
use GraphQL\Executor\ExecutionResult; |
13
|
|
|
use GraphQL\Executor\Executor; |
14
|
|
|
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; |
15
|
|
|
use GraphQL\Executor\Promise\PromiseAdapter; |
16
|
|
|
use GraphQL\Language\AST\DocumentNode; |
17
|
|
|
use GraphQL\Language\Parser; |
18
|
|
|
use GraphQL\Language\Visitor; |
19
|
|
|
use GraphQL\Server\Helper; |
20
|
|
|
use GraphQL\Server\OperationParams; |
21
|
|
|
use GraphQL\Server\RequestError; |
22
|
|
|
use GraphQL\Server\ServerConfig; |
23
|
|
|
use GraphQL\Type\Schema; |
24
|
|
|
use GraphQL\Utils\AST; |
25
|
|
|
use GraphQL\Utils\Utils; |
26
|
|
|
use GraphQL\Validator\DocumentValidator; |
27
|
|
|
|
28
|
|
|
class QueryProcessor { |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* The current user account. |
32
|
|
|
* |
33
|
|
|
* @var \Drupal\Core\Session\AccountProxyInterface |
34
|
|
|
*/ |
35
|
|
|
protected $currentUser; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The schema plugin manager. |
39
|
|
|
* |
40
|
|
|
* @var \Drupal\graphql\Plugin\SchemaPluginManager |
41
|
|
|
*/ |
42
|
|
|
protected $pluginManager; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* The query provider service. |
46
|
|
|
* |
47
|
|
|
* @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface |
48
|
|
|
*/ |
49
|
|
|
protected $queryProvider; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Processor constructor. |
53
|
|
|
* |
54
|
|
|
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser |
55
|
|
|
* The current user. |
56
|
|
|
* @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager |
57
|
|
|
* The schema plugin manager. |
58
|
|
|
* @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider |
59
|
|
|
* The query provider service. |
60
|
|
|
*/ |
61
|
|
|
public function __construct( |
62
|
|
|
AccountProxyInterface $currentUser, |
63
|
|
|
SchemaPluginManager $pluginManager, |
64
|
|
|
QueryProviderInterface $queryProvider |
65
|
|
|
) { |
66
|
|
|
$this->currentUser = $currentUser; |
67
|
|
|
$this->pluginManager = $pluginManager; |
68
|
|
|
$this->queryProvider = $queryProvider; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Processes one or multiple graphql operations. |
73
|
|
|
* |
74
|
|
|
* @param string $schema |
75
|
|
|
* The plugin id of the schema to use. |
76
|
|
|
* @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params |
77
|
|
|
* The graphql operation(s) to execute. |
78
|
|
|
* @param array $globals |
79
|
|
|
* The query context. |
80
|
|
|
* |
81
|
|
|
* @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[] |
82
|
|
|
* The query result. |
83
|
|
|
* |
84
|
|
|
*/ |
85
|
|
|
public function processQuery($schema, $params, array $globals = []) { |
86
|
|
|
// Load the plugin from the schema manager. |
87
|
|
|
$plugin = $this->pluginManager->createInstance($schema); |
88
|
|
|
$schema = $plugin->getSchema(); |
89
|
|
|
|
90
|
|
|
// If the current user has appropriate permissions, allow to bypass |
91
|
|
|
// the secure fields restriction. |
92
|
|
|
$globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security'); |
93
|
|
|
|
94
|
|
|
// Create the server config. |
95
|
|
|
$config = ServerConfig::create(); |
96
|
|
|
$config->setDebug(!empty($globals['development'])); |
97
|
|
|
$config->setSchema($schema); |
98
|
|
|
$config->setQueryBatching(TRUE); |
99
|
|
|
$config->setContext(function () use ($globals) { |
100
|
|
|
// Each document (e.g. in a batch query) gets its own resolve context but |
101
|
|
|
// the global parameters are shared. This allows us to collect the cache |
102
|
|
|
// metadata and contextual values (e.g. inheritance for language) for each |
103
|
|
|
// query separately. |
104
|
|
|
return new ResolveContext($globals); |
105
|
|
|
}); |
106
|
|
|
|
107
|
|
|
$config->setPersistentQueryLoader(function ($id, OperationParams $params) { |
108
|
|
|
if ($query = $this->queryProvider->getQuery($id, $params)) { |
109
|
|
|
return $query; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id)); |
113
|
|
|
}); |
114
|
|
|
|
115
|
|
|
if (is_array($params)) { |
116
|
|
|
return $this->executeBatch($config, $params); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return $this->executeSingle($config, $params); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
124
|
|
|
* @param \GraphQL\Server\OperationParams $params |
125
|
|
|
* |
126
|
|
|
* @return mixed |
127
|
|
|
*/ |
128
|
|
|
public function executeSingle(ServerConfig $config, OperationParams $params) { |
129
|
|
|
$adapter = new SyncPromiseAdapter(); |
130
|
|
|
$result = $this->executeOperation($adapter, $config, $params, FALSE); |
131
|
|
|
return $adapter->wait($result); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
136
|
|
|
* @param array $params |
137
|
|
|
* |
138
|
|
|
* @return mixed |
139
|
|
|
*/ |
140
|
|
|
public function executeBatch(ServerConfig $config, array $params) { |
141
|
|
|
$adapter = new SyncPromiseAdapter(); |
142
|
|
|
$result = array_map(function ($params) use ($adapter, $config) { |
143
|
|
|
return $this->executeOperation($adapter, $config, $params, TRUE); |
144
|
|
|
}, $params); |
145
|
|
|
|
146
|
|
|
$result = $adapter->all($result); |
147
|
|
|
return $adapter->wait($result); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
152
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
153
|
|
|
* @param \GraphQL\Server\OperationParams $params |
154
|
|
|
* @param bool $batching |
155
|
|
|
* |
156
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
157
|
|
|
*/ |
158
|
|
|
protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) { |
159
|
|
|
try { |
160
|
|
|
if (!$config->getSchema()) { |
161
|
|
|
throw new \LogicException('Missing schema for query execution.'); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
if ($batching && !$config->getQueryBatching()) { |
165
|
|
|
throw new RequestError('Batched queries are not supported by this server.'); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if ($errors = (new Helper())->validateOperationParams($params)) { |
169
|
|
|
$errors = Utils::map($errors, function (RequestError $err) { |
170
|
|
|
return Error::createLocatedError($err, NULL, NULL); |
171
|
|
|
}); |
172
|
|
|
|
173
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, $errors)); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$schema = $config->getSchema(); |
177
|
|
|
$variables = $params->variables; |
178
|
|
|
$operation = $params->operation; |
179
|
|
|
$document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query; |
180
|
|
|
if (!$document instanceof DocumentNode) { |
181
|
|
|
$document = Parser::parse($document); |
182
|
|
|
|
183
|
|
|
// Assume that pre-parsed documents are already validated. This allows |
184
|
|
|
// us to store pre-validated query documents e.g. for persisted queries |
185
|
|
|
// effectively improving performance by skipping run-time validation. |
186
|
|
|
$rules = $this->resolveValidationRules($config, $params, $document, $operation); |
187
|
|
|
if ($errors = DocumentValidator::validate($schema, $document, $rules)) { |
188
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, $errors)); |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if ($params->isReadOnly() && AST::getOperation($document, $operation) !== 'query') { |
193
|
|
|
throw new RequestError('GET requests are only supported for query operations.'); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
// TODO: Collect cache metadata from AST and perform a cach lookup. |
197
|
|
|
|
198
|
|
|
$resolver = $config->getFieldResolver(); |
199
|
|
|
$root = $this->resolveRootValue($config, $params, $document, $operation); |
200
|
|
|
$context = $this->resolveContextValue($config, $params, $document, $operation); |
201
|
|
|
$promise = Executor::promiseToExecute( |
202
|
|
|
$adapter, |
203
|
|
|
$schema, |
204
|
|
|
$document, |
205
|
|
|
$root, |
206
|
|
|
$context, |
207
|
|
|
$variables, |
208
|
|
|
$operation, |
209
|
|
|
$resolver |
210
|
|
|
); |
211
|
|
|
|
212
|
|
|
return $promise->then(function (ExecutionResult $result) use ($context) { |
213
|
|
|
$metadata = (new CacheableMetadata())->addCacheableDependency($context); |
214
|
|
|
return new QueryResult($result->data, $result->errors, $result->extensions, $metadata); |
215
|
|
|
}); |
216
|
|
|
} |
217
|
|
|
catch (RequestError $exception) { |
218
|
|
|
$result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)])); |
219
|
|
|
} |
220
|
|
|
catch (Error $exception) { |
221
|
|
|
$result = $adapter->createFulfilled(new QueryResult(NULL, [$exception])); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
return $result->then(function(QueryResult $result) use ($config) { |
225
|
|
|
if ($config->getErrorsHandler()) { |
226
|
|
|
$result->setErrorsHandler($config->getErrorsHandler()); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
if ($config->getErrorFormatter() || $config->getDebug()) { |
230
|
|
|
$result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug())); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return $result; |
234
|
|
|
}); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
239
|
|
|
* @param \GraphQL\Server\OperationParams $params |
240
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
241
|
|
|
* @param $operation |
242
|
|
|
* |
243
|
|
|
* @return callable|mixed |
244
|
|
|
*/ |
245
|
|
View Code Duplication |
protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
246
|
|
|
$root = $config->getRootValue(); |
247
|
|
|
if (is_callable($root)) { |
248
|
|
|
$root = $root($params, $document, $operation); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
return $root; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
256
|
|
|
* @param \GraphQL\Server\OperationParams $params |
257
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
258
|
|
|
* @param $operation |
259
|
|
|
* |
260
|
|
|
* @return callable|mixed |
261
|
|
|
*/ |
262
|
|
View Code Duplication |
protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
263
|
|
|
$context = $config->getContext(); |
264
|
|
|
if (is_callable($context)) { |
265
|
|
|
$context = $context($params, $document, $operation); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
return $context; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
273
|
|
|
* @param \GraphQL\Server\OperationParams $params |
274
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
275
|
|
|
* @param $operation |
276
|
|
|
* |
277
|
|
|
* @return array|callable |
278
|
|
|
*/ |
279
|
|
|
protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
280
|
|
|
// Allow customizing validation rules per operation: |
281
|
|
|
$rules = $config->getValidationRules(); |
282
|
|
|
if (is_callable($rules)) { |
283
|
|
|
$rules = $rules($params, $document, $operation); |
284
|
|
|
if (!is_array($rules)) { |
285
|
|
|
throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules))); |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return $rules; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
294
|
|
|
* @param \GraphQL\Server\OperationParams $params |
295
|
|
|
* |
296
|
|
|
* @return mixed |
297
|
|
|
* @throws \GraphQL\Server\RequestError |
298
|
|
|
*/ |
299
|
|
|
protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) { |
300
|
|
|
if (!$loader = $config->getPersistentQueryLoader()) { |
301
|
|
|
throw new RequestError('Persisted queries are not supported by this server.'); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$source = $loader($params->queryId, $params); |
305
|
|
|
if (!is_string($source) && !$source instanceof DocumentNode) { |
306
|
|
|
throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source))); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return $source; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
} |
313
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.