-
Notifications
You must be signed in to change notification settings - Fork 731
/
GraphQLExecutor.swift
509 lines (445 loc) · 16.4 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
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
import Foundation
#if !COCOAPODS
import ApolloAPI
#endif
@_spi(Execution)
public class ObjectExecutionInfo {
let rootType: any SelectionSet.Type
let variables: GraphQLOperation.Variables?
let schema: any SchemaMetadata.Type
private(set) var responsePath: ResponsePath = []
private(set) var cachePath: ResponsePath = []
fileprivate(set) var fulfilledFragments: Set<ObjectIdentifier>
fileprivate(set) var deferredFragments: Set<ObjectIdentifier> = []
fileprivate init(
rootType: any SelectionSet.Type,
variables: GraphQLOperation.Variables?,
schema: (any SchemaMetadata.Type),
responsePath: ResponsePath,
cachePath: ResponsePath
) {
self.rootType = rootType
self.variables = variables
self.schema = schema
self.responsePath = responsePath
self.cachePath = cachePath
self.fulfilledFragments = [ObjectIdentifier(rootType)]
}
fileprivate init(
rootType: any SelectionSet.Type,
variables: GraphQLOperation.Variables?,
schema: (any SchemaMetadata.Type),
withRootCacheReference root: CacheReference? = nil
) {
self.rootType = rootType
self.variables = variables
self.schema = schema
if let root = root {
cachePath = [root.key]
}
self.fulfilledFragments = [ObjectIdentifier(rootType)]
}
func runtimeObjectType(
for json: JSONObject
) -> Object? {
guard let __typename = json["__typename"] as? String else {
guard let objectType = rootType.__parentType as? Object else {
return nil
}
return schema.objectType(forTypename: objectType.typename)
}
return schema.objectType(forTypename: __typename)
}
}
/// Stores the information for executing a field and all duplicate fields on the same selection set.
///
/// 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.
@_spi(Execution)
public class FieldExecutionInfo {
let field: Selection.Field
let parentInfo: ObjectExecutionInfo
var mergedFields: [Selection.Field]
var responsePath: ResponsePath
let responseKeyForField: String
var cachePath: ResponsePath = []
private var _cacheKeyForField: String?
init(
field: Selection.Field,
parentInfo: ObjectExecutionInfo
) {
self.field = field
self.parentInfo = parentInfo
mergedFields = [field]
let responseKey = field.responseKey
responsePath = parentInfo.responsePath.appending(responseKey)
responseKeyForField = responseKey
}
fileprivate func computeCacheKeyAndPath() throws {
cachePath = try parentInfo.cachePath.appending(cacheKeyForField())
}
func cacheKeyForField() throws -> String {
guard let _cacheKeyForField else {
let cacheKey = try field.cacheKey(with: parentInfo.variables)
_cacheKeyForField = cacheKey
return cacheKey
}
return _cacheKeyForField
}
/// Computes the `ObjectExecutionInfo` and selections that should be used for
/// executing the child object.
///
/// - Note: There will only be child selections if the fields for this field info are
/// object type fields (objects; lists of objects; or non-null wrapped objects).
/// For scalar fields, the child selections will be an empty array.
fileprivate func computeChildExecutionData(
withRootType rootType: any RootSelectionSet.Type,
cacheKey: CacheKey?
) -> (ObjectExecutionInfo, [Selection]) {
// If the object has it's own cache key, reset the cache path to the key,
// rather than using the inherited cache path from the parent field.
let cachePath: ResponsePath = {
if let cacheKey { return [cacheKey] }
else { return self.cachePath }
}()
let childExecutionInfo = ObjectExecutionInfo(
rootType: rootType,
variables: parentInfo.variables,
schema: parentInfo.schema,
responsePath: responsePath,
cachePath: cachePath
)
var childSelections: [Selection] = []
mergedFields.forEach { field in
guard case let .object(selectionSet) = field.type.namedType else {
return
}
childExecutionInfo.fulfilledFragments.insert(ObjectIdentifier(selectionSet.self))
childSelections.append(contentsOf: selectionSet.__selections)
}
return (childExecutionInfo, childSelections)
}
func copy() -> FieldExecutionInfo {
FieldExecutionInfo(self)
}
private init(_ info: FieldExecutionInfo) {
self.field = info.field
self.parentInfo = info.parentInfo
self.mergedFields = info.mergedFields
self.responsePath = info.responsePath
self.responseKeyForField = info.responseKeyForField
self.cachePath = info.cachePath
self._cacheKeyForField = info._cacheKeyForField
}
}
/// An error which has occurred during GraphQL execution.
public struct GraphQLExecutionError: Error, LocalizedError {
let path: ResponsePath
public var pathString: String { path.description }
/// The error that occurred during parsing.
public let underlying: any Error
/// A description of the error which includes the path where the error occurred.
public var errorDescription: String? {
return "Error at 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 an 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 by `GraphQLDependencyTracker` keep track of dependent
/// keys.
///
/// The methods in this class closely follow the
/// [execution algorithm described in the GraphQL specification]
/// (http://spec.graphql.org/draft/#sec-Execution)
@_spi(Execution)
public final class GraphQLExecutor<Source: GraphQLExecutionSource> {
private let executionSource: Source
public init(executionSource: Source) {
self.executionSource = executionSource
}
// MARK: - Execution
@_spi(Execution)
public func execute<
Accumulator: GraphQLResultAccumulator,
SelectionSet: RootSelectionSet
>(
selectionSet: SelectionSet.Type,
on data: Source.RawObjectData,
withRootCacheReference root: CacheReference? = nil,
variables: GraphQLOperation.Variables? = nil,
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
return try execute(
selectionSet: selectionSet,
on: data,
withRootCacheReference: root,
variables: variables,
schema: SelectionSet.Schema.self,
accumulator: accumulator
)
}
func execute<
Accumulator: GraphQLResultAccumulator,
Operation: GraphQLOperation
>(
selectionSet: any SelectionSet.Type,
in operation: Operation.Type,
on data: Source.RawObjectData,
withRootCacheReference root: CacheReference? = nil,
variables: GraphQLOperation.Variables? = nil,
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
return try execute(
selectionSet: selectionSet,
on: data,
withRootCacheReference: root,
variables: variables,
schema: Operation.Data.Schema.self,
accumulator: accumulator
)
}
private func execute<
Accumulator: GraphQLResultAccumulator
>(
selectionSet: any SelectionSet.Type,
on data: Source.RawObjectData,
withRootCacheReference root: CacheReference? = nil,
variables: GraphQLOperation.Variables? = nil,
schema: (any SchemaMetadata.Type),
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
let info = ObjectExecutionInfo(
rootType: selectionSet,
variables: variables,
schema: schema,
withRootCacheReference: root
)
let rootValue: PossiblyDeferred<Accumulator.ObjectResult> = execute(
selections: selectionSet.__selections,
on: data,
info: info,
accumulator: accumulator
)
return try accumulator.finish(rootValue: try rootValue.get(), info: info)
}
private func execute<Accumulator: GraphQLResultAccumulator>(
selections: [Selection],
on object: Source.RawObjectData,
info: ObjectExecutionInfo,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.ObjectResult> {
let fieldEntries: [PossiblyDeferred<Accumulator.FieldEntry?>] = execute(
selections: selections,
on: object,
info: info,
accumulator: accumulator
)
return compactLazilyEvaluateAll(fieldEntries).map {
try accumulator.accept(fieldEntries: $0, info: info)
}
}
private func execute<Accumulator: GraphQLResultAccumulator>(
selections: [Selection],
on object: Source.RawObjectData,
info: ObjectExecutionInfo,
accumulator: Accumulator
) -> [PossiblyDeferred<Accumulator.FieldEntry?>] {
do {
let groupedFields = try groupFields(selections, on: object, info: info)
info.fulfilledFragments = groupedFields.fulfilledFragments
info.deferredFragments = []
var fieldEntries: [PossiblyDeferred<Accumulator.FieldEntry?>] = []
fieldEntries.reserveCapacity(groupedFields.count)
for (_, fields) in groupedFields.fieldInfoList {
let fieldEntry = execute(
fields: fields,
on: object,
accumulator: accumulator)
fieldEntries.append(fieldEntry)
}
if executionSource.shouldAttemptDeferredFragmentExecution {
for deferredFragment in groupedFields.deferredFragments {
guard let fragmentType = groupedFields.cachedFragmentIdentifierTypes[deferredFragment] else {
info.deferredFragments.insert(deferredFragment)
continue
}
do {
let deferredFragmentFieldEntries = try lazilyEvaluateAll(
execute(
selections: fragmentType.__selections,
on: object,
info: info,
accumulator: accumulator
)
)
.get()
.compactMap { PossiblyDeferred.immediate(.success($0)) }
fieldEntries.append(contentsOf: deferredFragmentFieldEntries)
info.fulfilledFragments.insert(deferredFragment)
} catch {
info.deferredFragments.insert(deferredFragment)
continue
}
}
} else {
info.deferredFragments = groupedFields.deferredFragments
}
return fieldEntries
} catch {
return [.immediate(.failure(error))]
}
}
/// Groups fields that share the same response key for simultaneous resolution.
///
/// 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 groupFields(
_ selections: [Selection],
on object: Source.RawObjectData,
info: ObjectExecutionInfo
) throws -> FieldSelectionGrouping {
var grouping = FieldSelectionGrouping(info: info)
try Source.FieldCollector.collectFields(
from: selections,
into: &grouping,
for: object,
info: info
)
return grouping
}
/// 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 fieldInfo: FieldExecutionInfo,
on object: Source.RawObjectData,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.FieldEntry?> {
if accumulator.requiresCacheKeyComputation {
do {
try fieldInfo.computeCacheKeyAndPath()
} catch {
return .immediate(.failure(error))
}
}
return executionSource.resolveField(with: fieldInfo, on: object)
.flatMap {
return self.complete(fields: fieldInfo,
withValue: $0,
accumulator: accumulator)
}.map {
try accumulator.accept(fieldEntry: $0, info: fieldInfo)
}.mapError { error in
if !(error is GraphQLExecutionError) {
return GraphQLExecutionError(path: fieldInfo.responsePath, underlying: error)
} else {
return error
}
}
}
private func complete<Accumulator: GraphQLResultAccumulator>(
fields fieldInfo: FieldExecutionInfo,
withValue value: JSONValue?,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
complete(fields: fieldInfo,
withValue: value,
asType: fieldInfo.field.type,
accumulator: accumulator)
}
/// 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>(
fields fieldInfo: FieldExecutionInfo,
withValue value: JSONValue?,
asType returnType: Selection.Field.OutputType,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
guard let value else {
return PossiblyDeferred { try accumulator.acceptMissingValue(info: fieldInfo) }
}
if value is NSNull && returnType.isNullable {
return PossiblyDeferred { try accumulator.acceptNullValue(info: fieldInfo) }
}
switch returnType {
case .nonNull where value is NSNull:
return .immediate(.failure(JSONDecodingError.nullValue))
case let .nonNull(innerType):
return complete(fields: fieldInfo,
withValue: value,
asType: innerType,
accumulator: accumulator)
case .scalar:
return PossiblyDeferred { try accumulator.accept(scalar: value, info: fieldInfo) }
case .customScalar:
return PossiblyDeferred { try accumulator.accept(customScalar: value, info: fieldInfo) }
case .list(let innerType):
guard let array = value as? [JSONValue] else {
return PossiblyDeferred { throw JSONDecodingError.wrongType }
}
let completedArray = array
.enumerated()
.map { index, element -> PossiblyDeferred<Accumulator.PartialResult> in
let elementFieldInfo = fieldInfo.copy()
let indexSegment = String(index)
elementFieldInfo.responsePath.append(indexSegment)
if accumulator.requiresCacheKeyComputation {
elementFieldInfo.cachePath.append(indexSegment)
}
return self
.complete(
fields: elementFieldInfo,
withValue: element,
asType: innerType,
accumulator: accumulator
)
.mapError { error in
if !(error is GraphQLExecutionError) {
return GraphQLExecutionError(path: elementFieldInfo.responsePath, underlying: error)
} else {
return error
}
}
}
return lazilyEvaluateAll(completedArray).map {
try accumulator.accept(list: $0, info: fieldInfo)
}
case let .object(rootSelectionSetType):
guard let object = value as? Source.RawObjectData else {
return PossiblyDeferred { throw JSONDecodingError.wrongType }
}
return executeChildSelections(
forObjectTypeFields: fieldInfo,
withRootType: rootSelectionSetType,
onChildObject: object,
accumulator: accumulator
)
}
}
private func executeChildSelections<Accumulator: GraphQLResultAccumulator>(
forObjectTypeFields fieldInfo: FieldExecutionInfo,
withRootType rootSelectionSetType: any RootSelectionSet.Type,
onChildObject object: Source.RawObjectData,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
let (childExecutionInfo, selections) = fieldInfo.computeChildExecutionData(
withRootType: rootSelectionSetType,
cacheKey: executionSource.computeCacheKey(for: object, in: fieldInfo.parentInfo.schema)
)
return execute(
selections: selections,
on: object,
info: childExecutionInfo,
accumulator: accumulator
)
.map { try accumulator.accept(childObject: $0, info: fieldInfo) }
}
}