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