-
Notifications
You must be signed in to change notification settings - Fork 731
/
GraphQLExecutor.swift
286 lines (238 loc) · 11.8 KB
/
GraphQLExecutor.swift
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
import Foundation
import Dispatch
/// A resolver is responsible for resolving a value for a field.
public typealias GraphQLResolver = (_ object: JSONObject, _ info: GraphQLResolveInfo) -> ResultOrPromise<JSONValue?>
public struct GraphQLResolveInfo {
let variables: GraphQLMap?
var responsePath: [String] = []
var responseKeyForField: String = ""
var cachePath: [String] = []
var cacheKeyForField: String = ""
var fields: [GraphQLField] = []
init(rootKey: CacheKey?, variables: GraphQLMap?) {
self.variables = variables
if let rootKey = rootKey {
cachePath = [rootKey]
}
}
}
func joined(path: [String]) -> String {
return path.joined(separator: ".")
}
public struct GraphQLResultError: Error, LocalizedError {
let path: [String]
let underlying: Error
public var errorDescription: String? {
return "Error at path \"\(joined(path: path))\": \(underlying)"
}
}
/// A GraphQL executor is responsible for executing a selection set and generating a result. It is initialized with a resolver closure that gets called repeatedly to resolve field values.
///
/// An executor is used both to parse a response received from the server, and to read from the normalized cache. It can also be configured with a accumulator that receives events during execution, and these execution events are used by `GraphQLResultNormalizer` to normalize a response into a flat set of records and keep track of dependent keys.
///
/// The methods in this class closely follow the [execution algorithm described in the GraphQL specification](https://facebook.github.io/graphql/#sec-Execution), but an important difference is that execution returns a value for every selection in a selection set, not the merged fields. This means we get a separate result for every fragment, even though all fields that share a response key are still executed at the same time for efficiency.
///
/// So given the following query:
///
/// ```
/// query HeroAndFriendsNames {
/// hero {
/// name
/// friends {
/// name
/// }
/// ...FriendsAppearsIn
/// }
/// }
///
/// fragment FriendsAppearsIn on Character {
/// friends {
/// appearsIn
/// }
/// }
/// ```
///
/// A server would return a response with `name` and `appearsIn` merged into one object:
///
/// ```
/// ...
/// {
/// "name": "R2-D2",
/// "friends": [
/// {
/// "name": "Luke Skywalker",
/// "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"]
/// }
/// }
/// ...
/// ```
///
/// The executor on the other hand, will return a separate value for every selection:
///
/// - `String`
/// - `[HeroAndFriendsNames.Data.Hero.Friend]`
/// - `FriendsAppearsIn`
/// - `[FriendsAppearsIn.Friend]`
///
/// These values then get passed into a generated `GraphQLMappable` initializer, and this is how type safe results get built up.
///
public final class GraphQLExecutor {
private let queue: DispatchQueue
private let resolver: GraphQLResolver
var dispatchDataLoads: (() -> Void)? = nil
var dispatchDataLoadsScheduled: Bool = false
var cacheKeyForObject: CacheKeyForObject?
var shouldComputeCachePath = true
/// Creates a GraphQLExecutor that resolves field values by calling the provided resolver.
public init(resolver: @escaping GraphQLResolver) {
queue = DispatchQueue(label: "com.apollographql.GraphQLExecutor")
self.resolver = resolver
}
private func runtimeType(of object: JSONObject) -> String? {
return object["__typename"] as? String
}
private func cacheKey(for object: JSONObject) -> String? {
guard let value = cacheKeyForObject?(object) else { return nil }
if let array = value as? [Any?] {
return array.compactMap { String(describing: $0) }.joined(separator: "_")
} else {
return String(describing: value)
}
}
// MARK: - Execution
func execute<Accumulator: GraphQLResultAccumulator>(selections: [GraphQLSelection], on object: JSONObject, withKey key: CacheKey? = nil, variables: GraphQLMap? = nil, accumulator: Accumulator) throws -> Promise<Accumulator.FinalResult> {
let info = GraphQLResolveInfo(rootKey: key, variables: variables)
return try execute(selections: selections, on: object, info: info, accumulator: accumulator).map {
try accumulator.finish(rootValue: $0, info: info)
}.asPromise()
}
private func execute<Accumulator: GraphQLResultAccumulator>(selections: [GraphQLSelection], on object: JSONObject, info: GraphQLResolveInfo, accumulator: Accumulator) throws -> ResultOrPromise<Accumulator.ObjectResult> {
var groupedFields = GroupedSequence<String, GraphQLField>()
try collectFields(selections: selections, forRuntimeType: runtimeType(of: object), into: &groupedFields, info: info)
var fieldEntries: [ResultOrPromise<Accumulator.FieldEntry>] = []
fieldEntries.reserveCapacity(groupedFields.keys.count)
for (_, fields) in groupedFields {
let fieldEntry = try execute(fields: fields, on: object, info: info, accumulator: accumulator)
fieldEntries.append(fieldEntry)
}
if let dispatchDataLoads = dispatchDataLoads, !dispatchDataLoadsScheduled {
dispatchDataLoadsScheduled = true
queue.async {
self.dispatchDataLoadsScheduled = false
dispatchDataLoads()
}
}
return whenAll(fieldEntries, notifyOn: queue).map {
try accumulator.accept(fieldEntries: $0, info: info)
}
}
/// Before execution, the selection set is converted to a grouped field set. Each entry in the grouped field set is a list of fields that share a response key. This ensures all fields with the same response key (alias or field name) included via referenced fragments are executed at the same time.
private func collectFields(selections: [GraphQLSelection], forRuntimeType runtimeType: String?, into groupedFields: inout GroupedSequence<String, GraphQLField>, info: GraphQLResolveInfo) throws {
for selection in selections {
switch selection {
case let field as GraphQLField:
_ = groupedFields.append(value: field, forKey: field.responseKey)
case let booleanCondition as GraphQLBooleanCondition:
guard let value = info.variables?[booleanCondition.variableName] else {
throw GraphQLError("Variable \(booleanCondition.variableName) was not provided.")
}
if value as? Bool == !booleanCondition.inverted {
try collectFields(selections: booleanCondition.selections, forRuntimeType: runtimeType, into: &groupedFields, info: info)
}
case let fragmentSpread as GraphQLFragmentSpread:
let fragment = fragmentSpread.fragment
if let runtimeType = runtimeType, fragment.possibleTypes.contains(runtimeType) {
try collectFields(selections: fragment.selections, forRuntimeType: runtimeType, into: &groupedFields, info: info)
}
case let typeCase as GraphQLTypeCase:
let selections: [GraphQLSelection]
if let runtimeType = runtimeType {
selections = typeCase.variants[runtimeType] ?? typeCase.default
} else {
selections = typeCase.default
}
try collectFields(selections: selections, forRuntimeType: runtimeType, into: &groupedFields, info: info)
default:
preconditionFailure()
}
}
}
/// Each field requested in the grouped field set that is defined on the selected objectType will result in an entry in the response map. Field execution first coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value.
private func execute<Accumulator: GraphQLResultAccumulator>(fields: [GraphQLField], on object: JSONObject, info: GraphQLResolveInfo, accumulator: Accumulator) throws -> ResultOrPromise<Accumulator.FieldEntry> {
// GraphQL validation makes sure all fields sharing the same response key have the same arguments and are of the same type, so we only need to resolve one field.
let firstField = fields[0]
var info = info
let responseKey = firstField.responseKey
info.responseKeyForField = responseKey
info.responsePath.append(responseKey)
if shouldComputeCachePath {
let cacheKey = try firstField.cacheKey(with: info.variables)
info.cacheKeyForField = cacheKey
info.cachePath.append(cacheKey)
}
// We still need all fields to complete the value, because they may have different selection sets.
info.fields = fields
let resultOrPromise = resolver(object, info)
return resultOrPromise.on(queue: queue).flatMap { value in
guard let value = value else {
throw JSONDecodingError.missingValue
}
return try self.complete(value: value, ofType: firstField.type, info: info, accumulator: accumulator)
}.map {
try accumulator.accept(fieldEntry: $0, info: info)
}.catch { error in
if !(error is GraphQLResultError) {
throw GraphQLResultError(path: info.responsePath, underlying: error)
}
}
}
/// After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the field execution process continues recursively.
private func complete<Accumulator: GraphQLResultAccumulator>(value: JSONValue, ofType returnType: GraphQLOutputType, info: GraphQLResolveInfo, accumulator: Accumulator) throws -> ResultOrPromise<Accumulator.PartialResult> {
if case .nonNull(let innerType) = returnType {
if value is NSNull {
return .result(.failure(JSONDecodingError.nullValue))
}
return try complete(value: value, ofType: innerType, info: info, accumulator: accumulator)
}
if value is NSNull {
return ResultOrPromise { try accumulator.acceptNullValue(info: info) }
}
switch returnType {
case .scalar:
return ResultOrPromise { try accumulator.accept(scalar: value, info: info) }
case .list(let innerType):
guard let array = value as? [JSONValue] else { return .result(.failure(JSONDecodingError.wrongType)) }
return try whenAll(array.enumerated().map { index, element -> ResultOrPromise<Accumulator.PartialResult> in
var info = info
let indexSegment = String(index)
info.responsePath.append(indexSegment)
info.cachePath.append(indexSegment)
return try self.complete(value: element, ofType: innerType, info: info, accumulator: accumulator)
}, notifyOn: queue).map { completedArray in
return try accumulator.accept(list: completedArray, info: info)
}
case .object:
guard let object = value as? JSONObject else { return .result(.failure(JSONDecodingError.wrongType)) }
// The merged selection set is a list of fields from all sub‐selection sets of the original fields.
let selections = mergeSelectionSets(for: info.fields)
var info = info
if shouldComputeCachePath, let cacheKeyForObject = self.cacheKey(for: object) {
info.cachePath = [cacheKeyForObject]
}
// We execute the merged selection set on the object to complete the value. This is the recursive step in the GraphQL execution model.
return try execute(selections: selections, on: object, info: info, accumulator: accumulator).map { return $0 as! Accumulator.PartialResult }
default:
preconditionFailure()
}
}
/// When fields are selected multiple times, their selection sets are merged together when completing the value in order to continue execution of the sub‐selection sets.
private func mergeSelectionSets(for fields: [GraphQLField]) -> [GraphQLSelection] {
var selections: [GraphQLSelection] = []
for field in fields {
if case let .object(fieldSelections) = field.type.namedType {
selections.append(contentsOf: fieldSelections)
}
}
return selections
}
}