1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\graphql\GraphQL\Execution; |
4
|
|
|
|
5
|
|
|
use Drupal\Core\Cache\CacheableMetadata; |
6
|
|
|
use Drupal\Core\Render\RenderContext; |
7
|
|
|
use Drupal\Core\Render\RendererInterface; |
8
|
|
|
use Drupal\Core\Session\AccountProxyInterface; |
9
|
|
|
use Drupal\graphql\Plugin\SchemaPluginManager; |
10
|
|
|
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface; |
11
|
|
|
use GraphQL\Error\Error; |
12
|
|
|
use GraphQL\Error\FormattedError; |
13
|
|
|
use GraphQL\Executor\ExecutionResult; |
14
|
|
|
use GraphQL\Executor\Executor; |
15
|
|
|
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; |
16
|
|
|
use GraphQL\Executor\Promise\PromiseAdapter; |
17
|
|
|
use GraphQL\GraphQL; |
18
|
|
|
use GraphQL\Language\AST\DocumentNode; |
19
|
|
|
use GraphQL\Language\Parser; |
20
|
|
|
use GraphQL\Language\Source; |
21
|
|
|
use GraphQL\Server\Helper; |
22
|
|
|
use GraphQL\Server\OperationParams; |
23
|
|
|
use GraphQL\Server\RequestError; |
24
|
|
|
use GraphQL\Server\ServerConfig; |
25
|
|
|
use GraphQL\Type\Schema; |
26
|
|
|
use GraphQL\Utils\AST; |
27
|
|
|
use GraphQL\Utils\Utils; |
28
|
|
|
use GraphQL\Validator\DocumentValidator; |
29
|
|
|
|
30
|
|
|
class QueryProcessor { |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* The current user account. |
34
|
|
|
* |
35
|
|
|
* @var \Drupal\Core\Session\AccountProxyInterface |
36
|
|
|
*/ |
37
|
|
|
protected $currentUser; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* The schema plugin manager. |
41
|
|
|
* |
42
|
|
|
* @var \Drupal\graphql\Plugin\SchemaPluginManager |
43
|
|
|
*/ |
44
|
|
|
protected $pluginManager; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* The query provider service. |
48
|
|
|
* |
49
|
|
|
* @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface |
50
|
|
|
*/ |
51
|
|
|
protected $queryProvider; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* The renderer service. |
55
|
|
|
* |
56
|
|
|
* @var \Drupal\Core\Render\RendererInterface |
57
|
|
|
*/ |
58
|
|
|
protected $renderer; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Processor constructor. |
62
|
|
|
* |
63
|
|
|
* @param \Drupal\Core\Render\RendererInterface $renderer |
64
|
|
|
* The renderer service. |
65
|
|
|
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser |
66
|
|
|
* The current user. |
67
|
|
|
* @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager |
68
|
|
|
* The schema plugin manager. |
69
|
|
|
* @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider |
70
|
|
|
* The query provider service. |
71
|
|
|
*/ |
72
|
|
|
public function __construct( |
73
|
|
|
RendererInterface $renderer, |
74
|
|
|
AccountProxyInterface $currentUser, |
75
|
|
|
SchemaPluginManager $pluginManager, |
76
|
|
|
QueryProviderInterface $queryProvider |
77
|
|
|
) { |
78
|
|
|
$this->renderer = $renderer; |
79
|
|
|
$this->currentUser = $currentUser; |
80
|
|
|
$this->pluginManager = $pluginManager; |
81
|
|
|
$this->queryProvider = $queryProvider; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Processes one or multiple graphql operations. |
86
|
|
|
* |
87
|
|
|
* @param string $schema |
88
|
|
|
* The plugin id of the schema to use. |
89
|
|
|
* @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params |
90
|
|
|
* The graphql operation(s) to execute. |
91
|
|
|
* @param mixed $context |
92
|
|
|
* The query context. |
93
|
|
|
* @param bool $debug |
94
|
|
|
* Whether to run this query in debugging mode. |
95
|
|
|
* |
96
|
|
|
* @return \Drupal\graphql\GraphQL\Execution\QueryResult |
97
|
|
|
* The query result. |
98
|
|
|
*/ |
99
|
|
|
public function processQuery($schema, $params, $context = NULL, $debug = FALSE) { |
100
|
|
|
// Load the plugin from the schema manager. |
101
|
|
|
$plugin = $this->pluginManager->createInstance($schema); |
102
|
|
|
$schema = $plugin->getSchema(); |
103
|
|
|
|
104
|
|
|
// Create the server config. |
105
|
|
|
$config = ServerConfig::create(); |
106
|
|
|
$config->setDebug($debug); |
107
|
|
|
$config->setSchema($schema); |
108
|
|
|
$config->setContext($context); |
109
|
|
|
$config->setQueryBatching(TRUE); |
110
|
|
|
$config->setPersistentQueryLoader(function ($id, OperationParams $params) { |
111
|
|
|
if ($query = $this->queryProvider->getQuery($id, $params)) { |
112
|
|
|
return $query; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id)); |
116
|
|
|
}); |
117
|
|
|
|
118
|
|
|
return $this->executeQuery($config, $params); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Executes one or multiple graphql operations. |
123
|
|
|
* |
124
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
125
|
|
|
* The server config. |
126
|
|
|
* @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params |
127
|
|
|
* The graphql operation(s) to execute. |
128
|
|
|
* |
129
|
|
|
* @return \Drupal\graphql\GraphQL\Execution\QueryResult |
130
|
|
|
* The result of executing the operations. |
131
|
|
|
*/ |
132
|
|
|
protected function executeQuery(ServerConfig $config, $params) { |
133
|
|
|
// Evaluating the request might lead to rendering of markup which in turn |
134
|
|
|
// might "leak" cache metadata. Therefore, we execute the request within a |
135
|
|
|
// render context and collect the leaked metadata afterwards. |
136
|
|
|
$context = new RenderContext(); |
137
|
|
|
/** @var \GraphQL\Executor\ExecutionResult|\GraphQL\Executor\ExecutionResult[] $result */ |
138
|
|
|
$result = $this->renderer->executeInRenderContext($context, function() use ($config, $params) { |
139
|
|
|
if (is_array($params)) { |
140
|
|
|
return $this->executeBatch($config, $params); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return $this->executeSingle($config, $params); |
144
|
|
|
}); |
145
|
|
|
|
146
|
|
|
$metadata = new CacheableMetadata(); |
147
|
|
|
// Apply render context cache metadata to the response. |
148
|
|
|
if (!$context->isEmpty()) { |
149
|
|
|
$metadata->addCacheableDependency($context->pop()); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
return new QueryResult($result, $metadata); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
157
|
|
|
* @param \GraphQL\Server\OperationParams $params |
158
|
|
|
* |
159
|
|
|
* @return mixed |
160
|
|
|
*/ |
161
|
|
|
public function executeSingle(ServerConfig $config, OperationParams $params) { |
162
|
|
|
$adapter = new SyncPromiseAdapter(); |
163
|
|
|
$result = $this->promiseToExecuteOperation($adapter, $config, $params, FALSE); |
164
|
|
|
return $adapter->wait($result); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
169
|
|
|
* @param array $params |
170
|
|
|
* |
171
|
|
|
* @return mixed |
172
|
|
|
*/ |
173
|
|
|
public function executeBatch(ServerConfig $config, array $params) { |
174
|
|
|
$adapter = new SyncPromiseAdapter(); |
175
|
|
|
$result = array_map(function ($params) use ($adapter, $config) { |
176
|
|
|
$this->promiseToExecuteOperation($adapter, $config, $params, TRUE); |
177
|
|
|
}, $params); |
178
|
|
|
|
179
|
|
|
$result = $adapter->all($result); |
180
|
|
|
return $adapter->wait($result); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
185
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
186
|
|
|
* @param \GraphQL\Server\OperationParams $params |
187
|
|
|
* @param bool $batching |
188
|
|
|
* |
189
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
190
|
|
|
*/ |
191
|
|
|
protected function promiseToExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) { |
192
|
|
|
try { |
193
|
|
|
if (!$config->getSchema()) { |
194
|
|
|
throw new \LogicException('Missing schema for query execution.'); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
if ($batching && !$config->getQueryBatching()) { |
198
|
|
|
throw new RequestError('Batched queries are not supported by this server.'); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if ($errors = (new Helper())->validateOperationParams($params)) { |
202
|
|
|
$errors = Utils::map($errors, function (RequestError $err) { |
203
|
|
|
return Error::createLocatedError($err, NULL, NULL); |
204
|
|
|
}); |
205
|
|
|
|
206
|
|
|
return $adapter->createFulfilled( |
207
|
|
|
new ExecutionResult(NULL, $errors) |
208
|
|
|
); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$variables = $params->variables; |
212
|
|
|
$operation = $params->operation; |
213
|
|
|
$document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query; |
214
|
|
|
if (!$document instanceof DocumentNode) { |
215
|
|
|
$document = Parser::parse($document); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
if ($params->isReadOnly() && AST::getOperation($document, $operation) !== 'query') { |
219
|
|
|
throw new RequestError('GET requests are only supported for query operations.'); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
$schema = $config->getSchema(); |
223
|
|
|
$resolver = $config->getFieldResolver(); |
224
|
|
|
$root = $this->resolveRootValue($config, $params, $document, $operation); |
225
|
|
|
$context = $this->resolveContextValue($config, $params, $document, $operation); |
226
|
|
|
$rules = $this->resolveValidationRules($config, $params, $document, $operation); |
227
|
|
|
$result = $this->promiseToExecute( |
228
|
|
|
$adapter, |
229
|
|
|
$schema, |
230
|
|
|
$document, |
231
|
|
|
$root, |
232
|
|
|
$context, |
233
|
|
|
$variables, |
234
|
|
|
$operation, |
235
|
|
|
$resolver, |
236
|
|
|
$rules |
237
|
|
|
); |
238
|
|
|
} |
239
|
|
|
catch (RequestError $exception) { |
240
|
|
|
$result = $adapter->createFulfilled(new ExecutionResult(NULL, [Error::createLocatedError($exception)])); |
241
|
|
|
} |
242
|
|
|
catch (Error $exception) { |
243
|
|
|
$result = $adapter->createFulfilled(new ExecutionResult(NULL, [$exception])); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return $result->then(function(ExecutionResult $result) use ($config) { |
247
|
|
|
if ($config->getErrorsHandler()) { |
248
|
|
|
$result->setErrorsHandler($config->getErrorsHandler()); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
if ($config->getErrorFormatter() || $config->getDebug()) { |
252
|
|
|
$result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug())); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $result; |
256
|
|
|
}); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
261
|
|
|
* @param \GraphQL\Type\Schema $schema |
262
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
263
|
|
|
* @param null $root |
264
|
|
|
* @param null $context |
265
|
|
|
* @param null $variables |
266
|
|
|
* @param null $operation |
267
|
|
|
* @param callable|NULL $resolver |
268
|
|
|
* @param array|NULL $rules |
269
|
|
|
* |
270
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
271
|
|
|
*/ |
272
|
|
|
protected function promiseToExecute( |
273
|
|
|
PromiseAdapter $adapter, |
274
|
|
|
Schema $schema, |
275
|
|
|
DocumentNode $document, |
276
|
|
|
$root = NULL, |
277
|
|
|
$context = NULL, |
278
|
|
|
$variables = NULL, |
279
|
|
|
$operation = NULL, |
280
|
|
|
callable $resolver = NULL, |
281
|
|
|
array $rules = NULL |
282
|
|
|
) { |
283
|
|
|
try { |
284
|
|
|
if ($errors = DocumentValidator::validate($schema, $document, $rules)) { |
|
|
|
|
285
|
|
|
return $adapter->createFulfilled(new ExecutionResult(NULL, $errors)); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
// TODO: Visit the document nodes, extract cache metadata and perform a cache lookup. |
289
|
|
|
return Executor::promiseToExecute($adapter, $schema, $document, $root, $context, $variables, $operation, $resolver); |
290
|
|
|
} |
291
|
|
|
catch (Error $exception) { |
292
|
|
|
return $adapter->createFulfilled(new ExecutionResult(NULL, [$exception])); |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
298
|
|
|
* @param \GraphQL\Server\OperationParams $params |
299
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
300
|
|
|
* @param $operation |
301
|
|
|
* |
302
|
|
|
* @return callable|mixed |
303
|
|
|
*/ |
304
|
|
View Code Duplication |
protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
305
|
|
|
$root = $config->getRootValue(); |
306
|
|
|
if (is_callable($root)) { |
307
|
|
|
$root = $root($params, $document, $operation); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $root; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
315
|
|
|
* @param \GraphQL\Server\OperationParams $params |
316
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
317
|
|
|
* @param $operation |
318
|
|
|
* |
319
|
|
|
* @return callable|mixed |
320
|
|
|
*/ |
321
|
|
View Code Duplication |
protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
322
|
|
|
$context = $config->getContext(); |
323
|
|
|
if (is_callable($context)) { |
324
|
|
|
$context = $context($params, $document, $operation); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
return $context; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
332
|
|
|
* @param \GraphQL\Server\OperationParams $params |
333
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
334
|
|
|
* @param $operation |
335
|
|
|
* |
336
|
|
|
* @return array|callable |
337
|
|
|
*/ |
338
|
|
|
protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
339
|
|
|
// Allow customizing validation rules per operation: |
340
|
|
|
$rules = $config->getValidationRules(); |
341
|
|
|
if (is_callable($rules)) { |
342
|
|
|
$rules = $rules($params, $document, $operation); |
343
|
|
|
if (!is_array($rules)) { |
344
|
|
|
throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules))); |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
return $rules; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
353
|
|
|
* @param \GraphQL\Server\OperationParams $params |
354
|
|
|
* |
355
|
|
|
* @return mixed |
356
|
|
|
* @throws \GraphQL\Server\RequestError |
357
|
|
|
*/ |
358
|
|
|
protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) { |
359
|
|
|
if (!$loader = $config->getPersistentQueryLoader()) { |
360
|
|
|
throw new RequestError('Persisted queries are not supported by this server.'); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$source = $loader($params->queryId, $params); |
364
|
|
|
if (!is_string($source) && !$source instanceof DocumentNode) { |
365
|
|
|
throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source))); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
return $source; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
} |
372
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.