-
Notifications
You must be signed in to change notification settings - Fork 162
/
federation.clj
142 lines (128 loc) · 5.86 KB
/
federation.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
; Copyright (c) 2020-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.federation
(:require
[com.walmartlabs.lacinia.resolve :as resolve :refer [with-error]]
[com.walmartlabs.lacinia.internal-utils :as utils]
[com.walmartlabs.lacinia.schema :as schema]
[clojure.spec.alpha :as s]))
(def foundation-types
"Map of annotations and types to automatically include into an SDL
schema used for federation."
{:scalars
{:_Any {:parse identity
:serialize identity}
:_FieldSet {:parse identity
:serialize identity}}
:objects
{:_Service
{:fields
{:sdl {:type '(non-null String)}}}}
:directive-defs
{:external {:locations #{:field-definition}}
:requires {:args {:fields {:type '(non-null :_FieldSet)}}
:locations #{:field-definition}}
:provides {:args {:fields {:type '(non-null :_FieldSet)}}
:locations #{:field-definition}}
:key {:args {:fields {:type '(non-null :_FieldSet)}}
:locations #{:object :interface}}
;; We will need this as the schema model doesn't
;; track the concept of "extends" (it's handled by
;; the SDL schema parser).
:extends {:locations #{:object :interface}}}})
(defn ^:private is-entity?
[type-def]
(some #(-> % :directive-type (= :key))
(:directives type-def)))
(defn ^:private find-entity-names
[schema]
(->> schema
:objects
(reduce-kv (fn [coll type-name type-def]
(if (is-entity? type-def)
(conj coll type-name)
coll))
[])
sort
seq))
(defn ^:private prevent-collision
[m ks]
(when (some? (get-in m ks))
(throw (IllegalStateException. (str "Key " (pr-str ks) " already exists in schema")))))
(defn ^:private entities-resolver-factory
"Entity resolvers are special resolvers. They are passed
the context, no args, and a seq of representations and return a seq
of entities for those representations.
entity-resolvers is a map of keyword to resolver fn (or FieldResolver instance)s."
[entity-names entity-resolvers]
(let [entity-names' (set entity-names)
actual (-> entity-resolvers keys set)
entity-resolvers' (utils/map-vals resolve/as-resolver-fn entity-resolvers)]
(when (not= entity-names' actual)
(throw (ex-info "Must provide entity resolvers for each entity (each type with @key)"
{:expected entity-names
:actual (sort actual)})))
(fn [context args _]
(let [{:keys [representations]} args
*errors (volatile! nil)
grouped (group-by :__typename representations)
results (reduce-kv
(fn [coll type-name reps]
(if-let [resolver (get entity-resolvers' (keyword type-name))]
(let [result (resolver context {} reps)
result' (if (resolve/is-resolver-result? result)
result
(resolve/resolve-as result))]
(conj coll result'))
;; Not found! This is a sanity check as an implementing service
;; should never be asked to resolve an entity it doesn't define (internal or external)
(do
(vswap! *errors conj {:message (str "No entity resolver for type " (utils/q type-name))})
coll)))
[]
grouped)
errors @*errors
maybe-wrap (fn [result]
(if errors
(with-error result errors)
result))]
;; Quick optimization; don't do the aggregation if there's only a single
;; result (very common case).
(case (count results)
0 (maybe-wrap [])
1 (if errors
(utils/transform-result (first results) #(with-error % errors))
(first results))
(utils/aggregate-results results #(maybe-wrap (reduce into [] %))))))))
(defn inject-federation
"Called after SDL parsing to extend the input schema
(not the compiled schema) with federation support."
[schema sdl entity-resolvers]
(let [entity-names (find-entity-names schema)
entities-resolver (entities-resolver-factory entity-names entity-resolvers)
query-root (get-in schema [:roots :query] :QueryRoot)]
(prevent-collision schema [:unions :_Entity])
(prevent-collision schema [:objects query-root :fields :_service])
(prevent-collision schema [:objects query-root :fields :_entities])
(cond-> (assoc-in schema [:objects query-root :fields :_service]
{:type '(non-null :_Service)
:resolve (fn [_ _ _] {:sdl sdl})})
entity-names (-> (assoc-in [:unions :_Entity :members] entity-names)
(assoc-in [:objects query-root :fields :_entities]
{:type '(non-null (list :_Entity))
:args
{:representations
{:type '(non-null (list (non-null :_Any)))}}
:resolve entities-resolver})))))
(s/def ::entity-resolvers (s/map-of simple-keyword? ::schema/resolve))