Skip to content

Commit 5074bb2

Browse files
refactor: Avoids double-parsing request query
This avoids double-parsing the request queries by pre-parsing, validating the schema and request separately, and using the GraphQL package's `execute` method directly instead of `graphql`.
1 parent aeef8e1 commit 5074bb2

1 file changed

Lines changed: 71 additions & 12 deletions

File tree

Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,32 @@ extension GraphQLHandler {
55
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#get
66
func handleGet(request: Request) async throws -> Response {
77
let graphQLRequest = try request.query.decode(GraphQLRequest.self)
8+
9+
let documentAST: Document
810
let operationType: OperationType
911
do {
10-
operationType = try graphQLRequest.operationType()
12+
documentAST = try parse(
13+
source: Source(body: graphQLRequest.query, name: graphQLRequest.operationName ?? "")
14+
)
15+
operationType = try Self.operationTypeOf(document: documentAST, named: graphQLRequest.operationName)
1116
} catch {
1217
// Indicates a request parsing error
1318
throw Abort(.badRequest, reason: error.localizedDescription)
1419
}
1520
guard operationType != .mutation else {
1621
throw Abort(.methodNotAllowed, reason: "Mutations using GET are disallowed")
1722
}
23+
1824
let graphQLContextComputationInputs = GraphQLContextComputationInputs<WebSocketInitResult>(
1925
vaporRequest: request,
2026
graphQLRequest: graphQLRequest,
2127
websocketInitResult: nil
2228
)
2329
let context = try await computeContext(graphQLContextComputationInputs)
2430
let result = await execute(
25-
graphQLRequest: graphQLRequest,
31+
documentAST: documentAST,
32+
operationName: graphQLRequest.operationName,
33+
variables: graphQLRequest.variables,
2634
context: context,
2735
additionalValidationRules: config.additionalValidationRules
2836
)
@@ -35,43 +43,75 @@ extension GraphQLHandler {
3543
throw Abort(.unsupportedMediaType, reason: "Missing `Content-Type` header")
3644
}
3745
let graphQLRequest = try request.content.decode(GraphQLRequest.self)
46+
47+
let documentAST: Document
48+
do {
49+
documentAST = try parse(
50+
source: Source(body: graphQLRequest.query, name: graphQLRequest.operationName ?? "")
51+
)
52+
} catch let error as GraphQLError {
53+
// Indicates a request parsing error
54+
let result = GraphQLResult(data: nil, errors: [error])
55+
return try encodeResponse(result: result, headers: request.headers)
56+
}
57+
3858
let graphQLContextComputationInputs = GraphQLContextComputationInputs<WebSocketInitResult>(
3959
vaporRequest: request,
4060
graphQLRequest: graphQLRequest,
4161
websocketInitResult: nil
4262
)
4363
let context = try await computeContext(graphQLContextComputationInputs)
4464
let result = await execute(
45-
graphQLRequest: graphQLRequest,
65+
documentAST: documentAST,
66+
operationName: graphQLRequest.operationName,
67+
variables: graphQLRequest.variables,
4668
context: context,
4769
additionalValidationRules: config.additionalValidationRules
4870
)
4971
return try encodeResponse(result: result, headers: request.headers)
5072
}
5173

5274
private func execute(
53-
graphQLRequest: GraphQLRequest,
75+
documentAST: Document,
76+
operationName: String?,
77+
variables: [String: Map],
5478
context: Context,
5579
additionalValidationRules: [@Sendable (ValidationContext) -> Visitor]
5680
) async -> GraphQLResult {
5781
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#validation
82+
83+
// Validate schema
84+
do {
85+
let schemaValidationErrors = try validateSchema(schema: schema)
86+
guard schemaValidationErrors.isEmpty else {
87+
return GraphQLResult(errors: schemaValidationErrors)
88+
}
89+
} catch {
90+
return GraphQLResult(errors: [GraphQLError(error)])
91+
}
92+
5893
let validationRules = GraphQL.specifiedRules + additionalValidationRules
94+
// Validate request
95+
let validationErrors = validate(
96+
schema: schema,
97+
ast: documentAST,
98+
rules: validationRules
99+
)
100+
guard validationErrors.isEmpty else {
101+
return GraphQLResult(errors: validationErrors)
102+
}
59103

60104
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#execution
61105
let result: GraphQLResult
62106
do {
63-
result = try await graphql(
107+
result = try await GraphQL.execute(
64108
schema: schema,
65-
request: graphQLRequest.query,
109+
documentAST: documentAST,
66110
rootValue: rootValue,
67111
context: context,
68-
variableValues: graphQLRequest.variables,
69-
operationName: graphQLRequest.operationName,
70-
validationRules: validationRules
112+
variableValues: variables,
113+
operationName: operationName
71114
)
72-
} catch let error as GraphQLError {
73-
// This indicates a request parsing error
74-
return GraphQLResult(data: nil, errors: [error])
75115
} catch {
76116
return GraphQLResult(
77117
data: nil,
@@ -122,4 +162,23 @@ extension GraphQLHandler {
122162
try response.content.encode(result, as: selectedMediaType)
123163
return response
124164
}
165+
166+
private static func operationTypeOf(document: Document, named operationName: String? = nil) throws -> OperationType {
167+
let operationDefinitions = document.definitions.filter { operation in
168+
operation.kind == .operationDefinition
169+
}.compactMap { operation in
170+
operation as? OperationDefinition
171+
}
172+
173+
// We don't have to validate the document and check for multiple/missing operations here
174+
// That will be done upon execution.
175+
let operationMatch = operationDefinitions.first { operation in
176+
if let operationName {
177+
return operationName == operation.name?.value
178+
} else {
179+
return true
180+
}
181+
}
182+
return operationMatch?.operation ?? .query
183+
}
125184
}

0 commit comments

Comments
 (0)