Skip to content

Commit 2ed4f00

Browse files
williambaileypaulofaria
authored andcommitted
Introduce persisted queries (#21)
* Instrumentation * Persisted Queries
1 parent b67c14e commit 2ed4f00

File tree

10 files changed

+672
-36
lines changed

10 files changed

+672
-36
lines changed

Sources/GraphQL/Execution/Execute.swift

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class ExecutionContext {
3131
let queryStrategy: QueryFieldExecutionStrategy
3232
let mutationStrategy: MutationFieldExecutionStrategy
3333
let subscriptionStrategy: SubscriptionFieldExecutionStrategy
34+
let instrumentation: Instrumentation
3435
public let schema: GraphQLSchema
3536
public let fragments: [String: FragmentDefinition]
3637
public let rootValue: Any
@@ -55,6 +56,7 @@ public final class ExecutionContext {
5556
queryStrategy: QueryFieldExecutionStrategy,
5657
mutationStrategy: MutationFieldExecutionStrategy,
5758
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
59+
instrumentation: Instrumentation,
5860
schema: GraphQLSchema,
5961
fragments: [String: FragmentDefinition],
6062
rootValue: Any,
@@ -66,6 +68,7 @@ public final class ExecutionContext {
6668
self.queryStrategy = queryStrategy
6769
self.mutationStrategy = mutationStrategy
6870
self.subscriptionStrategy = subscriptionStrategy
71+
self.instrumentation = instrumentation
6972
self.schema = schema
7073
self.fragments = fragments
7174
self.rootValue = rootValue
@@ -214,50 +217,89 @@ func execute(
214217
queryStrategy: QueryFieldExecutionStrategy,
215218
mutationStrategy: MutationFieldExecutionStrategy,
216219
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
220+
instrumentation: Instrumentation,
217221
schema: GraphQLSchema,
218222
documentAST: Document,
219223
rootValue: Any,
220224
contextValue: Any,
221225
variableValues: [String: Map] = [:],
222226
operationName: String? = nil
223227
) throws -> Map {
224-
// If a valid context cannot be created due to incorrect arguments,
225-
// this will throw an error.
226-
let context = try buildExecutionContext(
227-
queryStrategy: queryStrategy,
228-
mutationStrategy: mutationStrategy,
229-
subscriptionStrategy: subscriptionStrategy,
230-
schema: schema,
231-
documentAST: documentAST,
232-
rootValue: rootValue,
233-
contextValue: contextValue,
234-
rawVariableValues: variableValues,
235-
operationName: operationName
236-
)
237228

229+
let executeStarted = instrumentation.now
230+
let context: ExecutionContext
231+
do {
232+
// If a valid context cannot be created due to incorrect arguments,
233+
// this will throw an error.
234+
context = try buildExecutionContext(
235+
queryStrategy: queryStrategy,
236+
mutationStrategy: mutationStrategy,
237+
subscriptionStrategy: subscriptionStrategy,
238+
instrumentation: instrumentation,
239+
schema: schema,
240+
documentAST: documentAST,
241+
rootValue: rootValue,
242+
contextValue: contextValue,
243+
rawVariableValues: variableValues,
244+
operationName: operationName
245+
)
246+
} catch let error as GraphQLError {
247+
instrumentation.operationExecution(
248+
processId: processId(),
249+
threadId: threadId(),
250+
started: executeStarted,
251+
finished: instrumentation.now,
252+
schema: schema,
253+
document: documentAST,
254+
rootValue: rootValue,
255+
contextValue: contextValue,
256+
variableValues: variableValues,
257+
operation: nil,
258+
errors: [error],
259+
result: nil
260+
)
261+
throw error
262+
}
263+
264+
let executeResult: Map
265+
let executeErrors: [GraphQLError]
238266
do {
239267
let data = try executeOperation(
240268
exeContext: context,
241269
operation: context.operation,
242270
rootValue: rootValue
243271
)
244-
245272
var dataMap: Map = [:]
246-
247273
for (key, value) in data {
248274
dataMap[key] = try map(from: value)
249275
}
250-
251276
var result: [String: Map] = ["data": dataMap]
252-
253277
if !context.errors.isEmpty {
254278
result["errors"] = context.errors.map
255279
}
256-
257-
return .dictionary(result)
280+
executeResult = .dictionary(result)
281+
executeErrors = context.errors
258282
} catch let error as GraphQLError {
259-
return ["errors": [error].map]
283+
executeResult = ["errors": [error].map]
284+
executeErrors = [error]
260285
}
286+
287+
instrumentation.operationExecution(
288+
processId: processId(),
289+
threadId: threadId(),
290+
started: executeStarted,
291+
finished: instrumentation.now,
292+
schema: schema,
293+
document: documentAST,
294+
rootValue: rootValue,
295+
contextValue: contextValue,
296+
variableValues: variableValues,
297+
operation: context.operation,
298+
errors: executeErrors,
299+
result: executeResult
300+
)
301+
302+
return executeResult
261303
}
262304

263305
/**
@@ -270,6 +312,7 @@ func buildExecutionContext(
270312
queryStrategy: QueryFieldExecutionStrategy,
271313
mutationStrategy: MutationFieldExecutionStrategy,
272314
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
315+
instrumentation: Instrumentation,
273316
schema: GraphQLSchema,
274317
documentAST: Document,
275318
rootValue: Any,
@@ -323,6 +366,7 @@ func buildExecutionContext(
323366
queryStrategy: queryStrategy,
324367
mutationStrategy: mutationStrategy,
325368
subscriptionStrategy: subscriptionStrategy,
369+
instrumentation: instrumentation,
326370
schema: schema,
327371
fragments: fragments,
328372
rootValue: rootValue,
@@ -629,6 +673,8 @@ public func resolveField(
629673
variableValues: exeContext.variableValues
630674
)
631675

676+
let resolveFieldStarted = exeContext.instrumentation.now
677+
632678
// Get the resolve func, regardless of if its result is normal
633679
// or abrupt (error).
634680
let result = resolveOrError(
@@ -638,6 +684,18 @@ public func resolveField(
638684
context: context,
639685
info: info
640686
)
687+
688+
exeContext.instrumentation.fieldResolution(
689+
processId: processId(),
690+
threadId: threadId(),
691+
started: resolveFieldStarted,
692+
finished: exeContext.instrumentation.now,
693+
source: source,
694+
args: args,
695+
context: context,
696+
info: info,
697+
result: result
698+
)
641699

642700
return try completeValueCatchingError(
643701
exeContext: exeContext,
@@ -649,9 +707,9 @@ public func resolveField(
649707
)
650708
}
651709

652-
enum ResultOrError {
653-
case result(Any?)
654-
case error(Error)
710+
public enum ResultOrError<T, E> {
711+
case result(T)
712+
case error(E)
655713
}
656714

657715
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
@@ -662,7 +720,7 @@ func resolveOrError(
662720
args: Map,
663721
context: Any,
664722
info: GraphQLResolveInfo
665-
)-> ResultOrError {
723+
)-> ResultOrError<Any?, Error> {
666724
do {
667725
return try .result(resolve(source, args, context, info))
668726
} catch {
@@ -678,7 +736,7 @@ func completeValueCatchingError(
678736
fieldASTs: [Field],
679737
info: GraphQLResolveInfo,
680738
path: [IndexPathElement],
681-
result: ResultOrError
739+
result: ResultOrError<Any?, Error>
682740
) throws -> Any? {
683741
// If the field type is non-nullable, then it is resolved without any
684742
// protection from errors, however it still properly locates the error.
@@ -724,7 +782,7 @@ func completeValueWithLocatedError(
724782
fieldASTs: [Field],
725783
info: GraphQLResolveInfo,
726784
path: [IndexPathElement],
727-
result: ResultOrError
785+
result: ResultOrError<Any?, Error>
728786
) throws -> Any? {
729787
do {
730788
let completed = try completeValue(
@@ -773,7 +831,7 @@ func completeValue(
773831
fieldASTs: [Field],
774832
info: GraphQLResolveInfo,
775833
path: [IndexPathElement],
776-
result: ResultOrError
834+
result: ResultOrError<Any?, Error>
777835
) throws -> Any? {
778836
switch result {
779837
case .error(let error):

Sources/GraphQL/GraphQL.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/// - parameter queryStrategy: The field execution strategy to use for query requests
1010
/// - parameter mutationStrategy: The field execution strategy to use for mutation requests
1111
/// - parameter subscriptionStrategy: The field execution strategy to use for subscription requests
12+
/// - parameter instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages.
1213
/// - parameter schema: The GraphQL type system to use when validating and executing a query.
1314
/// - parameter request: A GraphQL language formatted string representing the requested operation.
1415
/// - parameter rootValue: The value provided as the first argument to resolver functions on the top level type (e.g. the query object type).
@@ -23,16 +24,18 @@ public func graphql(
2324
queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(),
2425
mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(),
2526
subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(),
27+
instrumentation: Instrumentation = NoOpInstrumentation,
2628
schema: GraphQLSchema,
2729
request: String,
2830
rootValue: Any = Void(),
2931
contextValue: Any = Void(),
3032
variableValues: [String: Map] = [:],
3133
operationName: String? = nil
3234
) throws -> Map {
35+
3336
let source = Source(body: request, name: "GraphQL request")
34-
let documentAST = try parse(source: source)
35-
let validationErrors = validate(schema: schema, ast: documentAST)
37+
let documentAST = try parse(instrumentation: instrumentation, source: source)
38+
let validationErrors = validate(instrumentation: instrumentation, schema: schema, ast: documentAST)
3639

3740
guard validationErrors.isEmpty else {
3841
return ["errors": try validationErrors.asMap()]
@@ -42,6 +45,7 @@ public func graphql(
4245
queryStrategy: queryStrategy,
4346
mutationStrategy: mutationStrategy,
4447
subscriptionStrategy: subscriptionStrategy,
48+
instrumentation: instrumentation,
4549
schema: schema,
4650
documentAST: documentAST,
4751
rootValue: rootValue,
@@ -50,3 +54,55 @@ public func graphql(
5054
operationName: operationName
5155
)
5256
}
57+
58+
/// This is the primary entry point function for fulfilling GraphQL operations
59+
/// by using persisted queries.
60+
///
61+
/// - parameter queryStrategy: The field execution strategy to use for query requests
62+
/// - parameter mutationStrategy: The field execution strategy to use for mutation requests
63+
/// - parameter subscriptionStrategy: The field execution strategy to use for subscription requests
64+
/// - parameter instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages.
65+
/// - parameter queryRetrieval: The PersistedQueryRetrieval instance to use for looking up queries
66+
/// - parameter queryId: The id of the query to execute
67+
/// - parameter rootValue: The value provided as the first argument to resolver functions on the top level type (e.g. the query object type).
68+
/// - parameter contextValue: A context value provided to all resolver functions functions
69+
/// - parameter variableValues: A mapping of variable name to runtime value to use for all variables defined in the `request`.
70+
/// - parameter operationName: The name of the operation to use if `request` contains multiple possible operations. Can be omitted if `request` contains only one operation.
71+
///
72+
/// - throws: throws GraphQLError if an error occurs while parsing the `request`.
73+
///
74+
/// - returns: returns a `Map` dictionary containing the result of the query inside the key `data` and any validation or execution errors inside the key `errors`. The value of `data` might be `null` if, for example, the query is invalid. It's possible to have both `data` and `errors` if an error occurs only in a specific field. If that happens the value of that field will be `null` and there will be an error inside `errors` specifying the reason for the failure and the path of the failed field.
75+
public func graphql<Retrieval:PersistedQueryRetrieval>(
76+
queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(),
77+
mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(),
78+
subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(),
79+
instrumentation: Instrumentation = NoOpInstrumentation,
80+
queryRetrieval: Retrieval,
81+
queryId: Retrieval.Id,
82+
rootValue: Any = Void(),
83+
contextValue: Any = Void(),
84+
variableValues: [String: Map] = [:],
85+
operationName: String? = nil
86+
) throws -> Map {
87+
switch try queryRetrieval.lookup(queryId) {
88+
case .unknownId(_):
89+
throw GraphQLError(message: "Unknown query id")
90+
case .parseError(let parseError):
91+
throw parseError
92+
case .validateErrors(_, let validationErrors):
93+
return ["errors": try validationErrors.asMap()]
94+
case .result(let schema, let documentAST):
95+
return try execute(
96+
queryStrategy: queryStrategy,
97+
mutationStrategy: mutationStrategy,
98+
subscriptionStrategy: subscriptionStrategy,
99+
instrumentation: instrumentation,
100+
schema: schema,
101+
documentAST: documentAST,
102+
rootValue: rootValue,
103+
contextValue: contextValue,
104+
variableValues: variableValues,
105+
operationName: operationName
106+
)
107+
}
108+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Dispatch
2+
3+
/// Proxies calls through to another `Instrumentation` instance via a DispatchQueue
4+
///
5+
/// Has two primary use cases:
6+
/// 1. Allows a non thread safe Instrumentation implementation to be used along side a multithreaded execution strategy
7+
/// 2. Allows slow or heavy instrumentation processing to happen outside of the current query execution
8+
public class DispatchQueueInstrumentationWrapper: Instrumentation {
9+
10+
let instrumentation:Instrumentation
11+
let dispatchQueue: DispatchQueue
12+
let dispatchGroup: DispatchGroup?
13+
14+
public init(_ instrumentation: Instrumentation, label: String = "GraphQL instrumentation wrapper", qos: DispatchQoS = .utility, attributes: DispatchQueue.Attributes = [], dispatchGroup: DispatchGroup? = nil ) {
15+
self.instrumentation = instrumentation
16+
self.dispatchQueue = DispatchQueue(label: label, qos: qos, attributes: attributes)
17+
self.dispatchGroup = dispatchGroup
18+
}
19+
20+
public init(_ instrumentation: Instrumentation, dispatchQueue: DispatchQueue, dispatchGroup: DispatchGroup? = nil ) {
21+
self.instrumentation = instrumentation
22+
self.dispatchQueue = dispatchQueue
23+
self.dispatchGroup = dispatchGroup
24+
}
25+
26+
public var now: DispatchTime {
27+
return instrumentation.now
28+
}
29+
30+
public func queryParsing(processId: Int, threadId: Int, started: DispatchTime, finished: DispatchTime, source: Source, result: ResultOrError<Document, GraphQLError>) {
31+
dispatchQueue.async(group: dispatchGroup) {
32+
self.instrumentation.queryParsing(processId: processId, threadId: threadId, started: started, finished: finished, source: source, result: result)
33+
}
34+
}
35+
36+
public func queryValidation(processId: Int, threadId: Int, started: DispatchTime, finished: DispatchTime, schema: GraphQLSchema, document: Document, errors: [GraphQLError]) {
37+
dispatchQueue.async(group: dispatchGroup) {
38+
self.instrumentation.queryValidation(processId: processId, threadId: threadId, started: started, finished: finished, schema: schema, document: document, errors: errors)
39+
}
40+
}
41+
42+
public func operationExecution(processId: Int, threadId: Int, started: DispatchTime, finished: DispatchTime, schema: GraphQLSchema, document: Document, rootValue: Any, contextValue: Any, variableValues: [String : Map], operation: OperationDefinition?, errors: [GraphQLError], result: Map) {
43+
dispatchQueue.async(group: dispatchGroup) {
44+
self.instrumentation.operationExecution(processId: processId, threadId: threadId, started: started, finished: finished, schema: schema, document: document, rootValue: rootValue, contextValue: contextValue, variableValues: variableValues, operation: operation, errors: errors, result: result)
45+
}
46+
}
47+
48+
public func fieldResolution(processId: Int, threadId: Int, started: DispatchTime, finished: DispatchTime, source: Any, args: Map, context: Any, info: GraphQLResolveInfo, result: ResultOrError<Any?, Error>) {
49+
dispatchQueue.async(group: dispatchGroup) {
50+
self.instrumentation.fieldResolution(processId: processId, threadId: threadId, started: started, finished: finished, source: source, args: args, context: context, info: info, result: result)
51+
}
52+
}
53+
54+
}
55+

0 commit comments

Comments
 (0)