/
executor.clj
653 lines (557 loc) · 29.2 KB
/
executor.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
; Copyright (c) 2017-present Walmart, Inc.
;
; Licensed under the Apache License, Version 2.0 (the "License")
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
(ns com.walmartlabs.lacinia.executor
"Mechanisms for executing parsed queries against compiled schemas."
(:require
[com.walmartlabs.lacinia.internal-utils
:refer [cond-let map-vals remove-vals q aggregate-results transform-result]]
[flatland.ordered.map :refer [ordered-map]]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.resolve :as resolve
:refer [resolve-as resolve-promise]]
[com.walmartlabs.lacinia.selector-context :as sc]
[com.walmartlabs.lacinia.constants :as constants])
(:import
(clojure.lang PersistentQueue)))
(defn ^:private ex-info-map
[field-selection execution-context]
(remove-vals nil? {:locations [(:location field-selection)]
:path (:path execution-context)
:arguments (:reportable-arguments field-selection)}))
(defn ^:private assert-and-wrap-error
"An error returned by a resolver should be nil, a map, or a collection
of maps, and the map(s) must contain at least a :message key with a string value.
Returns nil, or a collection of one or more valid error maps."
[error-map-or-maps]
(cond
(nil? error-map-or-maps)
nil
(and (sequential? error-map-or-maps)
(every? (comp string? :message)
error-map-or-maps))
error-map-or-maps
(string? (:message error-map-or-maps))
[error-map-or-maps]
:else
(throw (ex-info (str "Errors must be nil, a map, or a sequence of maps "
"each containing, at minimum, a :message key and a string value.")
{:error error-map-or-maps}))))
(defn ^:private structured-error-map
"Converts an error map and extra data about location, path, etc. into the
correct format: top level keys :message, :path, and :location, and anything else
under a :extensions key."
[error-map extra-data]
(let [{:keys [message extensions]} error-map
{:keys [locations path]} extra-data
extensions' (merge (dissoc error-map :message :extensions)
(dissoc extra-data :locations :path)
extensions)]
(cond-> {:message message
:locations locations
:path path}
(seq extensions') (assoc :extensions extensions'))))
(defn ^:private enhance-errors
"From an error map, or a collection of error maps, add additional data to
each error, including location and arguments. Returns a seq of error maps."
[field-selection execution-context error-or-errors]
(let [errors-seq (assert-and-wrap-error error-or-errors)]
(when errors-seq
(let [extra-data (ex-info-map field-selection execution-context)]
(map #(structured-error-map % extra-data) errors-seq)))))
(defn ^:private field-selection-resolver
"Returns the field resolver for the provided field selection.
When the field-selection is on a concrete type, the resolve from the
nested field-definition is returned.
When the field selection is on an abstract type (an interface or union),
then the concrete type is extracted from the value instead, and the corresponding
field of the concrete type is used as the source for the field resolver."
[schema field-selection resolved-type value]
(cond-let
(:concrete-type? field-selection)
(-> field-selection :field-definition :resolve)
:let [{:keys [field]} field-selection]
(nil? resolved-type)
(throw (ex-info "Sanity check: value type tag is nil on abstract type."
{:value value}))
:let [type (get schema resolved-type)]
(nil? type)
(throw (ex-info "Sanity check: invalid type tag on value."
{:type-name resolved-type
:value value}))
:else
(or (get-in type [:fields field :resolve])
(throw (ex-info "Sanity check: field not present."
{:type resolved-type
:value value})))))
(defn ^:private invoke-resolver-for-field
"Resolves the value for a field selection node.
Returns a ResolverResult.
Optionally updates the timings inside the execution-context with start/finish/elapsed time
(in milliseconds). Timing checks only occur when enabled (timings is non-nil)
and not for default resolvers."
[execution-context field-selection]
(let [*timings (:*timings execution-context)
{:keys [arguments]} field-selection
container-value (:resolved-value execution-context)
{:keys [context]} execution-context
schema (get context constants/schema-key)
resolved-type (:resolved-type execution-context)
resolve-context (assoc context
:com.walmartlabs.lacinia/container-type-name resolved-type
constants/selection-key field-selection)
field-resolver (field-selection-resolver schema field-selection resolved-type container-value)
start-ms (when (and (some? *timings)
(not (-> field-resolver meta ::schema/default-resolver?)))
(System/currentTimeMillis))
resolver-result (field-resolver resolve-context arguments container-value)]
;; If not collecting timing results, then the resolver-result is all we need.
;; Otherwise, we need to create an extra promise so that we can observe the
;; delivery of the value to update our timing information. The downside is
;; that collecting timing information affects timing.
(if-not start-ms
resolver-result
(transform-result resolver-result
(fn [resolved-value]
(let [finish-ms (System/currentTimeMillis)
elapsed-ms (- finish-ms start-ms)]
;; Discard 0 and 1 ms results
(when (<= 2 elapsed-ms)
(swap! *timings conj {:start (str start-ms)
:finish (str finish-ms)
:path (:path execution-context)
;; This is just a convenience:
:elapsed elapsed-ms})))
resolved-value)))))
(declare ^:private resolve-and-select)
(defrecord ExecutionContext
;; context, resolved-value, and resolved-type change constantly during the process
;; *errors is an Atom containing a vector, which accumulates
;; error-maps during execution.
;; *warnings is an Atom containing a vector of warnings (error maps that
;; appear in the result as [:extensions :warnings].
;; *timings is usually nil, or may be an Atom containing an empty map, which
;; *extensions is an atom containing a map, if non-empty, it is added to the result map as :extensions
;; accumulates timing data during execution.
;; path is used when reporting errors
[context resolved-value resolved-type *errors *warnings *extensions *timings path])
(defn ^:private null-to-nil
[v]
(cond
(vector? v)
(map null-to-nil v)
(= ::null v)
nil
:else
v))
(defn ^:private propogate-nulls
"When all values for a selected value are ::null, it is replaced with
::null (if non-nullable) or nil (if nullable).
Otherwise, the selected values are a mix of real values and ::null, so replace
the ::null values with nil."
[non-nullable? selected-value]
(cond
;; This sometimes happens when a field returns multiple scalars:
(not (map? selected-value))
selected-value
(and (seq selected-value)
(every? (fn [[_ v]] (= v ::null))
selected-value))
(if non-nullable? ::null nil)
:else
(map-vals null-to-nil selected-value)))
(defrecord ^:private ResultTuple [alias value])
(defn ^:private apply-field-selection
[execution-context field-selection]
(let [{:keys [alias]} field-selection
non-nullable-field? (-> field-selection :field-definition :type :kind (= :non-null))
resolver-result (resolve-and-select execution-context field-selection)]
(transform-result resolver-result
(fn [resolved-field-value]
(let [sub-selection (cond
(and non-nullable-field?
(nil? resolved-field-value))
::null
;; child field was non-nullable and resolved to null,
;; but parent is nullable so let's null parent
(and (= resolved-field-value ::null)
(not non-nullable-field?))
nil
(map? resolved-field-value)
(propogate-nulls non-nullable-field? resolved-field-value)
;; TODO: We also support sets
(vector? resolved-field-value)
(mapv #(propogate-nulls non-nullable-field? %) resolved-field-value)
:else
resolved-field-value)]
(->ResultTuple alias sub-selection))))))
(defn ^:private maybe-apply-fragment
[execution-context fragment-selection concrete-types]
(let [actual-type (:resolved-type execution-context)]
(when (contains? concrete-types actual-type)
(resolve-and-select execution-context fragment-selection))))
(defn ^:private apply-inline-fragment
[execution-context inline-fragment-selection]
(maybe-apply-fragment execution-context
inline-fragment-selection
(:concrete-types inline-fragment-selection)))
(defn ^:private apply-fragment-spread
[execution-context fragment-spread-selection]
(let [{:keys [fragment-name]} fragment-spread-selection
fragment-def (get-in execution-context [:context constants/parsed-query-key :fragments fragment-name])]
(maybe-apply-fragment execution-context
;; A bit of a hack:
(assoc fragment-spread-selection
:selections (:selections fragment-def))
(:concrete-types fragment-def))))
(defn ^:private apply-selection
[execution-context selection]
(when-not (:disabled? selection)
(case (:selection-type selection)
:field (apply-field-selection execution-context selection)
:inline-fragment (apply-inline-fragment execution-context selection)
:fragment-spread (apply-fragment-spread execution-context selection))))
(defn ^:private merge-selected-values
"Merges the left and right values, with a special case for when the right value
is an ResultTuple."
[left-value right-value]
(if (instance? ResultTuple right-value)
(assoc left-value (:alias right-value) (:value right-value))
(merge left-value right-value)))
(defn ^:private execute-nested-selections
"Executes nested sub-selections once a value is resolved.
Returns a ResolverResult whose value is a map of keys and selected values."
[execution-context sub-selections]
;; First step is easy: convert the selections into ResolverResults.
;; Then once all the individual results are ready, combine them in the correct order.
(let [selection-results (keep #(apply-selection execution-context %) sub-selections)]
(aggregate-results selection-results
(fn [values]
(reduce merge-selected-values (ordered-map) values)))))
(defn ^:private combine-selection-results-sync
[execution-context previous-resolved-result sub-selection]
;; Let's just call the previous result "left" and the sub-selection's result "right".
;; However, sometimes a selection is disabled and returns nil instead of a ResolverResult.
(let [next-result (resolve-promise)]
(resolve/on-deliver! previous-resolved-result
(fn [left-value]
;; This is what makes it sync: we don't kick off the evaluation of the selection
;; until the previous selection, left, has completed.
(let [sub-resolved-result (apply-selection execution-context sub-selection)]
(resolve/on-deliver! sub-resolved-result
(fn [right-value]
(resolve/deliver! next-result
(merge-selected-values left-value right-value)))))))
;; This will deliver after the sub-selection delivers, which is only after the previous resolved result
;; delivers.
next-result))
(defn ^:private execute-nested-selections-sync
"Used to execute top-level mutation operations in synchronous order.
sub-selections is the sequence of top-level operations to execute with disabled operations
removed.
Returns ResolverResult whose value is a map of keys and selected values."
[execution-context sub-selections]
;; This could be optimized for the very common case of a single sub-selection.
(reduce #(combine-selection-results-sync execution-context %1 %2)
(resolve-as (ordered-map))
sub-selections))
(defn ^:private resolve-and-select
"Recursive resolution of a field within a containing field's resolved value.
Returns a ResolverResult of the selected value.
Accumulates errors in the execution context as a side-effect."
[execution-context selection]
(let [is-fragment? (-> selection :selection-type (not= :field))
;; When starting to execute a field, add the
execution-context' (if is-fragment?
execution-context
(update execution-context :path conj (:alias selection)))
sub-selections (:selections selection)
apply-errors (fn [selection-context sc-key ec-atom-key]
(when-let [errors (get selection-context sc-key)]
(->> errors
(mapcat #(enhance-errors selection execution-context' %))
(swap! (get execution-context' ec-atom-key) into))))
;; The selector pipeline validates the resolved value and handles things like iterating over
;; seqs before (repeatedly) invoking the callback, at which point, it is possible to
;; perform a recursive selection on the nested fields of the origin field.
selector-callback
(fn selector-callback [{:keys [resolved-value resolved-type execution-context] :as selection-context}]
;; Any errors from the resolver (via with-errors) or anywhere along the
;; selection pipeline are enhanced and added to the execution context.
(apply-errors selection-context :errors :*errors)
(apply-errors selection-context :warnings :*warnings)
(if (and (or (= [] (:path execution-context)) (some? resolved-value))
resolved-type
(seq sub-selections))
(execute-nested-selections
(assoc execution-context
:resolved-value resolved-value
:resolved-type resolved-type)
sub-selections)
(resolve-as resolved-value)))
;; In a concrete type, we know the selector from the field definition
;; (a field definition on a concrete object type). Otherwise, we need
;; to use the type of the parent node's resolved value, just
;; as we do to get a resolver.
resolved-type (:resolved-type execution-context')
selector (if is-fragment?
schema/floor-selector
(or (-> selection :field-definition :selector)
(let [field-name (:field selection)]
(-> execution-context'
:context
(get constants/schema-key)
(get resolved-type)
:fields
(get field-name)
:selector
(or (throw (ex-info "Sanity check: no selector."
{:type-name resolved-type
:selection selection})))))))
process-resolved-value (fn [resolved-value]
(loop [resolved-value resolved-value
selector-context (sc/->SelectorContext execution-context'
selector-callback
nil
nil)]
(if (sc/is-wrapped-value? resolved-value)
(recur (:value resolved-value)
(sc/apply-wrapped-value selector-context resolved-value))
;; Finally to a real value, not a wrapper. The commands may have
;; modified the :errors or :execution-context keys, and the pipeline
;; will do the rest. Errors will be dealt with in the callback.
(-> selector-context
(assoc :callback selector-callback
:resolved-value resolved-value)
selector))))
direct-fn (-> selection :field-definition :direct-fn)]
;; For fragments, we start with a single value and it passes right through to
;; sub-selections, without changing value or type.
(cond
is-fragment?
(selector (sc/->SelectorContext execution-context'
selector-callback
(:resolved-value execution-context')
resolved-type))
;; Optimization: for simple fields there may be direct function.
;; This is a function that synchronously provides the value from the container resolved value.
;; This is almost always a default resolver. The extracted value is passed though to
;; the selector, which returns a ResolverResult. Thus we've peeled back at least one layer
;; of ResolveResultPromise.
direct-fn
(-> execution-context'
:resolved-value
direct-fn
process-resolved-value)
;; Here's where it comes together. The field's selector
;; does the validations, and for list types, does the mapping.
;; It also figures out the field type.
;; Eventually, individual values will be passed to the callback, which can then turn around
;; and recurse down a level. The result is a map or a list of maps.
:else
(let [final-result (resolve-promise)]
(resolve/on-deliver! (invoke-resolver-for-field execution-context' selection)
(fn receive-resolved-value-from-field [resolved-value]
(resolve/on-deliver! (process-resolved-value resolved-value)
(fn deliver-selection-for-field [resolved-value]
(resolve/deliver! final-result resolved-value)))))
final-result))))
(defn execute-query
"Entrypoint for execution of a query.
Expects the context to contain the schema and parsed query.
Returns a ResolverResult that will deliver the result map.
This should generally not be invoked by user code; see [[execute-parsed-query]]."
[context]
(let [parsed-query (get context constants/parsed-query-key)
{:keys [selections operation-type]} parsed-query
enabled-selections (remove :disabled? selections)
*errors (atom [])
*warnings (atom [])
*extensions (atom {})
*timings (when (:com.walmartlabs.lacinia/enable-timing? context)
(atom []))
context' (assoc context constants/schema-key
(get parsed-query constants/schema-key))
;; Outside of subscriptions, the ::resolved-value is nil.
;; For subscriptions, the :resolved-value will be set to a non-nil value before
;; executing the query.
execution-context (map->ExecutionContext {:context context'
:*errors *errors
:*warnings *warnings
:*timings *timings
:*extensions *extensions
:path []
:resolved-type (get-in parsed-query [:root :type-name])
:resolved-value (::resolved-value context)})
result-promise (resolve-promise)
executor resolve/*callback-executor*
f (bound-fn []
(try
(let [operation-result (if (= :mutation operation-type)
(execute-nested-selections-sync execution-context enabled-selections)
(execute-nested-selections execution-context enabled-selections))]
(resolve/on-deliver! operation-result
(fn [selected-data]
(let [data (propogate-nulls false selected-data)]
(let [errors (seq @*errors)
warnings (seq @*warnings)
extensions @*extensions]
(resolve/deliver! result-promise
(cond-> {:data data}
(seq extensions) (assoc :extensions extensions)
*timings (assoc-in [:extensions :timings] @*timings)
errors (assoc :errors (distinct errors))
warnings (assoc-in [:extensions :warnings] (distinct warnings)))))))))
(catch Throwable t
(resolve/deliver! result-promise t))))]
(if executor
(.execute executor f)
(future (f)))
result-promise))
(defn invoke-streamer
"Given a parsed and prepared query (inside the context, as with [[execute-query]]),
this will locate the streamer for a subscription
and invoke it, passing it the context, the subscription arguments, and the source stream."
{:added "0.19.0"}
[context source-stream]
(let [parsed-query (get context constants/parsed-query-key)
{:keys [selections operation-type]} parsed-query
selection (do
(assert (= :subscription operation-type))
(first selections))
streamer (get-in selection [:field-definition :stream])]
(streamer context (:arguments selection) source-stream)))
(defn ^:private node-selections
[parsed-query node]
(case (:selection-type node)
(:field :inline-fragment) (:selections node)
:fragment-spread
(let [{:keys [fragment-name]} node]
(get-in parsed-query [:fragments fragment-name :selections]))))
(defn ^:private to-field-name
"Identifies the qualified field name for a selection node. May return nil
for meta-fields such as __typename."
[node]
(get-in node [:field-definition :qualified-name]))
(defn ^:private walk-selections
[context node-xform]
(let [parsed-query (get context constants/parsed-query-key)
selection (get context constants/selection-key)
step (fn step [queue]
(when (seq queue)
(let [node (peek queue)]
(cons node
(step (into (pop queue)
(node-selections parsed-query node)))))))]
(->> (conj PersistentQueue/EMPTY selection)
step
;; remove the first node (the selection); just interested
;; in what's beneath the selection
next
(filter #(= :field (:selection-type %)))
(keep node-xform))))
(defn selections-seq
"A width-first traversal of the selections tree, returning a lazy sequence
of qualified field names. A qualified field name is a namespaced keyword,
the namespace is the containing type, e.g. :User/name.
Fragments are flattened (as if always selected)."
{:added "0.17.0"}
[context]
(walk-selections context to-field-name))
(defn ^:private to-field-data
[node]
(let [{:keys [field alias arguments]} node]
(cond-> {:name (to-field-name node)}
(not (= field alias)) (assoc :alias alias)
(seq arguments) (assoc :args arguments))))
(defn selections-seq2
"An enhancement of [[selections-seq]] that returns a map for each node:
:name
: The qualified field name
:args
: The arguments of the field (if any)
:alias
: The alias for the field, if any"
{:added "0.34.0"}
[context]
(walk-selections context to-field-data))
(defn selects-field?
"Invoked by a field resolver to determine if a particular field is selected anywhere within the selection
tree (that is, at any depth)."
{:added "0.17.0"}
[context field-name]
(boolean (some #(= field-name %) (selections-seq context))))
(defn ^:private conjv
[coll v]
(if (nil? coll)
(vector v)
(conj coll v)))
(defn ^:private intov
[coll v]
(if (nil? coll)
v
(into coll v)))
(defn ^:private build-selections-map
"Builds the selections map for a field selection node."
[parsed-query selections]
(reduce (fn [m selection]
(case (:selection-type selection)
:field
(if-some [field-name (to-field-name selection)]
(let [{:keys [field alias selections]} selection
arguments (:arguments selection)
selections-map (build-selections-map parsed-query selections)
nested-map (cond-> nil
(not (= field alias)) (assoc :alias alias)
(seq arguments) (assoc :args arguments)
(seq selections-map) (assoc :selections selections-map))]
(update m field-name conjv nested-map))
m)
:inline-fragment
(merge-with intov m (build-selections-map parsed-query (:selections selection)))
:fragment-spread
(let [{:keys [fragment-name]} selection
fragment-selections (get-in parsed-query [:fragments fragment-name :selections])]
(merge-with intov m (build-selections-map parsed-query fragment-selections)))))
{}
selections))
(defn selections-tree
"Constructs a tree of the selections below the current field.
Returns a map where the keys are qualified field names (the selections for this field).
The value is a vector of maps with three optional keys; :args, :alias, and :selections.
:args is the arguments that will be passed to that field's resolver.
:selections is the nested selection tree.
:alias is the alias for the field (most fields do not have aliases).
A vector is returned because the selection for an outer field may, via aliases, reference
the same inner field multiple times (with different arguments, aliases, and/or sub-selections).
Each key of a nested map is present only if a value is provided; for scalar fields with no arguments, the
nested map will be nil.
Fragments are flattened into containing fields, as with `selections-seq`."
{:added "0.17.0"}
[context]
(let [parsed-query (get context constants/parsed-query-key)
selection (get context constants/selection-key)]
(build-selections-map parsed-query (:selections selection))))
(defn parsed-query->context
"Converts a parsed query, prior to execution, into a context compatible with preview API:
* [[selections-tree]]
* [[selects-field?]]
* [[selections-seq]]
This is used to preview the execution of the query prior to execution."
{:added "0.34.0"}
[parsed-query]
(let [{:keys [root selections]} parsed-query]
{constants/parsed-query-key parsed-query
constants/selection-key {:selection-type :field
:field-definition root
:selections selections}}))