-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGraphQLHandler+HTTP.swift
More file actions
152 lines (136 loc) · 6.39 KB
/
Copy pathGraphQLHandler+HTTP.swift
File metadata and controls
152 lines (136 loc) · 6.39 KB
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
import Foundation
import GraphQL
import HTTPTypes
import Hummingbird
import NIOCore
extension GraphQLHandler {
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#get
func handleGet(request: Request, context: Context) async throws -> Response {
guard config.allowGet else {
throw HTTPError(.methodNotAllowed, message: "GET requests are disallowed")
}
// Decode query parameters as GraphQLRequest
let graphQLRequest = try request.uri.decodeQuery(as: GraphQLRequest.self, context: context)
let operationType: OperationType
do {
operationType = try graphQLRequest.operationType()
} catch {
// Indicates a request parsing error
throw HTTPError(.badRequest, message: error.localizedDescription)
}
guard operationType != .mutation else {
throw HTTPError(.methodNotAllowed, message: "Mutations using GET are disallowed")
}
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
hummingbirdRequest: request,
hummingbirdContext: context,
graphQLRequest: graphQLRequest,
websocketInitResult: nil
)
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
let result = await execute(
graphQLRequest: graphQLRequest,
context: graphQLContext,
additionalValidationRules: config.additionalValidationRules
)
return try encodeResponse(result: result, request: request, context: context)
}
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#post
func handlePost(request: Request, context: Context) async throws -> Response {
guard let contentType = request.headers[.contentType] else {
throw HTTPError(.unsupportedMediaType, message: "Missing `Content-Type` header")
}
guard let mediaType = MediaType(from: contentType) else { throw HTTPError(.badRequest) }
let graphQLRequest: GraphQLRequest
switch mediaType {
case .applicationJson, .applicationJsonGraphQL:
do {
graphQLRequest = try await config.coders.jsonDecoder.decode(GraphQLRequest.self, from: request, context: context)
} catch {
throw HTTPError(.badRequest, message: error.localizedDescription)
}
case .applicationUrlEncoded:
do {
graphQLRequest = try await config.coders.urlEncodedFormDecoder.decode(GraphQLRequest.self, from: request, context: context)
} catch {
throw HTTPError(.badRequest, message: error.localizedDescription)
}
default:
throw HTTPError(.unsupportedMediaType)
}
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
hummingbirdRequest: request,
hummingbirdContext: context,
graphQLRequest: graphQLRequest,
websocketInitResult: nil
)
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
let result = await execute(
graphQLRequest: graphQLRequest,
context: graphQLContext,
additionalValidationRules: config.additionalValidationRules
)
return try encodeResponse(result: result, request: request, context: context)
}
private func execute(
graphQLRequest: GraphQLRequest,
context: GraphQLContext,
additionalValidationRules: [@Sendable (ValidationContext) -> Visitor]
) async -> GraphQLResult {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#validation
let validationRules = GraphQL.specifiedRules + additionalValidationRules
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#execution
let result: GraphQLResult
do {
result = try await graphql(
schema: schema,
request: graphQLRequest.query,
rootValue: rootValue,
context: context,
variableValues: graphQLRequest.variables,
operationName: graphQLRequest.operationName,
validationRules: validationRules
)
} catch let error as GraphQLError {
// This indicates a request parsing error
return GraphQLResult(data: nil, errors: [error])
} catch {
return GraphQLResult(data: nil, errors: [GraphQLError(message: error.localizedDescription)])
}
return result
}
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#body
private func encodeResponse(result: GraphQLResult, request: Request, context: Context) throws -> Response {
let acceptHeader = request.headers[.accept]
if !config.allowMissingAcceptHeader, acceptHeader == nil {
throw HTTPError(.notAcceptable, message: "An `Accept` header must be provided")
}
let acceptedTypes = parseAcceptHeader(acceptHeader)
// Try to respond with the best matching media type, in order
for mediaType in acceptedTypes {
if MediaType.applicationJsonGraphQL.isType(mediaType) {
return try config.coders.graphQLJSONEncoder.encode(result, from: request, context: context)
}
if MediaType.applicationJson.isType(mediaType) {
return try config.coders.jsonEncoder.encode(result, from: request, context: context)
}
if MediaType.applicationUrlEncoded.isType(mediaType) {
return try config.coders.urlEncodedFormEncoder.encode(result, from: request, context: context)
}
}
// Use the default if configured to do so
if config.allowMissingAcceptHeader {
return try config.coders.graphQLJSONEncoder.encode(result, from: request, context: context)
}
// Fail
throw HTTPError(.notAcceptable)
}
private func parseAcceptHeader(_ header: String?) -> [MediaType] {
guard let header = header else { return [] }
return header
.split(separator: ",")
.compactMap { segment in
MediaType(from: segment.trimmingCharacters(in: .whitespaces))
}
}
}