Skip to content

Commit 8ea0982

Browse files
Merge pull request #11 from GraphQLSwift/feat/graphqlws-v1
Includes WebSocketInitResult in context computation inputs
2 parents cdb055e + c6dbf62 commit 8ea0982

10 files changed

Lines changed: 212 additions & 76 deletions

File tree

Examples/HelloWorld/Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ let package = Package(
1616
dependencies: [
1717
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
1818
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.0.0"),
19-
.package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "0.2.1"),
20-
.package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "0.2.1"),
19+
.package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "1.0.0"),
20+
.package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "1.0.0"),
2121
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
2222
.package(url: "https://github.com/hummingbird-project/hummingbird-websocket.git", from: "2.0.0"),
2323
],

Sources/GraphQLHummingbird/GraphQLConfig.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import GraphQL
33
import Hummingbird
44

55
/// Configuration options for GraphQLHummingbird
6-
public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Sendable {
6+
public struct GraphQLConfig<
7+
Context: RequestContext,
8+
WebSocketInit: Equatable & Codable & Sendable,
9+
WebSocketInitResult: Sendable
10+
>: Sendable {
711
let allowGet: Bool
812
let allowMissingAcceptHeader: Bool
913
let coders: Coders
@@ -28,7 +32,7 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
2832
subscriptionProtocols: Set<SubscriptionProtocol> = [.websocket],
2933
websocket: WebSocket = .init(
3034
// Including this strongly-typed argument is required to avoid compiler failures on Swift 6.2.3.
31-
onWebSocketInit: { (_: EmptyWebSocketInit) in }
35+
onWebSocketInit: { (_: EmptyWebSocketInit, _: Request, _: Context) in }
3236
),
3337
additionalValidationRules: [@Sendable (ValidationContext) -> Visitor] = []
3438
) {
@@ -94,14 +98,14 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
9498
}
9599

96100
public struct WebSocket: Sendable {
97-
let onWebSocketInit: @Sendable (WebSocketInit) async throws -> Void
101+
let onWebSocketInit: @Sendable (WebSocketInit, Request, Context) async throws -> WebSocketInitResult
98102

99103
/// GraphQL over WebSocket configuration
100104
/// - Parameter onWebSocketInit: A custom callback run during `connection_init` resolution that allows
101105
/// authorization using the `payload` field of the `connection_init` message.
102106
/// Throw from this closure to indicate that authorization has failed.
103107
public init(
104-
onWebSocketInit: @Sendable @escaping (WebSocketInit) async throws -> Void = { (_: EmptyWebSocketInit) in }
108+
onWebSocketInit: @Sendable @escaping (WebSocketInit, Request, Context) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit, _: Request, _: Context) in }
105109
) {
106110
self.onWebSocketInit = onWebSocketInit
107111
}

Sources/GraphQLHummingbird/GraphQLHandler.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,30 @@ import Hummingbird
44
struct GraphQLHandler<
55
Context: RequestContext,
66
GraphQLContext: Sendable,
7-
WebSocketInit: Equatable & Codable & Sendable
7+
WebSocketInit: Equatable & Codable & Sendable,
8+
WebSocketInitResult: Sendable
89
>: Sendable {
910
let schema: GraphQLSchema
1011
let rootValue: any Sendable
11-
let config: GraphQLConfig<WebSocketInit>
12-
let computeContext: @Sendable (GraphQLContextComputationInputs<Context>) async throws -> GraphQLContext
12+
let config: GraphQLConfig<Context, WebSocketInit, WebSocketInitResult>
13+
let computeContext: @Sendable (GraphQLContextComputationInputs<Context, WebSocketInitResult>) async throws -> GraphQLContext
1314
}
1415

16+
/// Request metadata that can be used to construct a GraphQL context
1517
public struct GraphQLContextComputationInputs<
16-
Context: RequestContext
18+
Context: RequestContext,
19+
WebSocketInitResult: Sendable
1720
>: Sendable {
21+
/// The Hummingbird request that initiated the GraphQL request. In WebSockets, this is the upgrade GET request.
1822
public let hummingbirdRequest: Request
23+
24+
/// The Hummingbird context from the request that initiated the GraphQL request. In WebSockets, this is the context from the upgrade GET request.
1925
public let hummingbirdContext: Context
26+
27+
/// The decoded GraphQL request, including the raw query, variables, and more
2028
public let graphQLRequest: GraphQLRequest
29+
30+
/// The result of the WebSocket's initialization closure. This can be used to customize GraphQL context creation based on the init
31+
/// message metadata as opposed to only the upgrade request. In non-WebSocket contexts, this is nil.
32+
public let websocketInitResult: WebSocketInitResult?
2133
}

Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ extension GraphQLHandler {
2424
guard operationType != .mutation else {
2525
throw HTTPError(.methodNotAllowed, message: "Mutations using GET are disallowed")
2626
}
27-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
27+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
2828
hummingbirdRequest: request,
2929
hummingbirdContext: context,
30-
graphQLRequest: graphQLRequest
30+
graphQLRequest: graphQLRequest,
31+
websocketInitResult: nil
3132
)
3233
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
3334
let result = await execute(
@@ -63,10 +64,11 @@ extension GraphQLHandler {
6364
throw HTTPError(.unsupportedMediaType)
6465
}
6566

66-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
67+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
6768
hummingbirdRequest: request,
6869
hummingbirdContext: context,
69-
graphQLRequest: graphQLRequest
70+
graphQLRequest: graphQLRequest,
71+
websocketInitResult: nil
7072
)
7173
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
7274
let result = await execute(

Sources/GraphQLHummingbird/Router+graphql.swift

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public extension RouterMethods {
1919
_ path: RouterPath = "graphql",
2020
schema: GraphQLSchema,
2121
rootValue: any Sendable = (),
22-
config: GraphQLConfig<EmptyWebSocketInit> = .init(),
23-
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<Context>) async throws -> GraphQLContext
22+
config: GraphQLConfig<Context, EmptyWebSocketInit, Void> = .init(),
23+
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<Context, Void>) async throws -> GraphQLContext
2424
) -> Self {
2525
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request
26-
let handler = GraphQLHandler<Context, GraphQLContext, EmptyWebSocketInit>(
26+
let handler = GraphQLHandler<Context, GraphQLContext, EmptyWebSocketInit, Void>(
2727
schema: schema,
2828
rootValue: rootValue,
2929
config: config,
@@ -78,15 +78,16 @@ public extension RouterMethods where Context: WebSocketRequestContext {
7878
@discardableResult
7979
func graphqlWebSocket<
8080
GraphQLContext: Sendable,
81-
WebSocketInit: Equatable & Codable & Sendable
81+
WebSocketInit: Equatable & Codable & Sendable,
82+
WebSocketInitResult: Sendable
8283
>(
8384
_ path: RouterPath = "graphql",
8485
schema: GraphQLSchema,
8586
rootValue: any Sendable = (),
86-
config: GraphQLConfig<WebSocketInit> = GraphQLConfig<EmptyWebSocketInit>(),
87-
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<Context>) async throws -> GraphQLContext
87+
config: GraphQLConfig<Context, WebSocketInit, WebSocketInitResult>,
88+
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<Context, WebSocketInitResult>) async throws -> GraphQLContext
8889
) -> Self {
89-
let handler = GraphQLHandler<Context, GraphQLContext, WebSocketInit>(
90+
let handler = GraphQLHandler<Context, GraphQLContext, WebSocketInit, WebSocketInitResult>(
9091
schema: schema,
9192
rootValue: rootValue,
9293
config: config,
@@ -107,4 +108,34 @@ public extension RouterMethods where Context: WebSocketRequestContext {
107108
}
108109
return self
109110
}
111+
112+
/// Registers a graphql websocket route that responds using the provided schema.
113+
///
114+
/// WebSocket requests support the
115+
/// [`graphql-transport-ws`](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)
116+
/// and [`graphql-ws`](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md)
117+
/// subprotocols.
118+
///
119+
/// - Parameters:
120+
/// - path: The route that should respond to GraphQL requests. Both `GET` and `POST` routes are registered.
121+
/// - schema: The GraphQL schema that should be used to respond to requests.
122+
/// - rootValue: The `rootValue` GraphQL execution arg. This is the object passed to the root resolvers.
123+
/// - computeContext: A closure used to compute the GraphQL context from incoming requests. This must be provided.
124+
@discardableResult
125+
func graphqlWebSocket<GraphQLContext: Sendable>(
126+
_ path: RouterPath = "graphql",
127+
schema: GraphQLSchema,
128+
rootValue: any Sendable = (),
129+
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<Context, Void>) async throws -> GraphQLContext
130+
) -> Self {
131+
// This is just an overload that allows not passing `config`, since we cannot use a default argument that
132+
// uses `Context` without getting `error: generic parameter 'Self' could not be inferred` compilation errors
133+
graphqlWebSocket(
134+
path,
135+
schema: schema,
136+
rootValue: rootValue,
137+
config: GraphQLConfig<Context, EmptyWebSocketInit, Void>(),
138+
computeContext: computeContext
139+
)
140+
}
110141
}

Sources/GraphQLHummingbird/WebSocket/GraphQLHandler+handleWebSocket.swift

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,32 @@ extension GraphQLHandler where Context: WebSocketRequestContext {
1414
subProtocol: WebSocketSubProtocol,
1515
logger: Logger
1616
) async throws {
17-
let messenger = WebSocketMessenger(inbound: inbound, outbound: outbound, logger: logger)
17+
let messenger = WebSocketMessenger(outbound: outbound, logger: logger)
18+
19+
// TODO: Make maxSize configurable
20+
let messageStream = inbound.messages(maxSize: 1024 * 1024).compactMap { message -> String? in
21+
// TODO: Add binary support
22+
guard case let .text(text) = message else {
23+
return nil
24+
}
25+
logger.trace("GraphQL server received: \(message)")
26+
return text
27+
}
1828

1929
switch subProtocol {
2030
case .graphqlTransportWs:
2131
// https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
22-
let server = GraphQLTransportWS.Server<WebSocketInit, AsyncThrowingStream<GraphQLResult, Error>>(
32+
let server = GraphQLTransportWS.Server<WebSocketInit, WebSocketInitResult, AsyncThrowingStream<GraphQLResult, Error>>(
2333
messenger: messenger,
24-
onExecute: { graphQLRequest in
25-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
34+
onInit: { initPayload in
35+
try await config.websocket.onWebSocketInit(initPayload, context.request, context.requestContext)
36+
},
37+
onExecute: { graphQLRequest, initResult in
38+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
2639
hummingbirdRequest: context.request,
2740
hummingbirdContext: context.requestContext,
28-
graphQLRequest: graphQLRequest
41+
graphQLRequest: graphQLRequest,
42+
websocketInitResult: initResult
2943
)
3044
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
3145
return try await graphql(
@@ -37,11 +51,12 @@ extension GraphQLHandler where Context: WebSocketRequestContext {
3751
operationName: graphQLRequest.operationName
3852
)
3953
},
40-
onSubscribe: { graphQLRequest in
41-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
54+
onSubscribe: { graphQLRequest, initResult in
55+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
4256
hummingbirdRequest: context.request,
4357
hummingbirdContext: context.requestContext,
44-
graphQLRequest: graphQLRequest
58+
graphQLRequest: graphQLRequest,
59+
websocketInitResult: initResult
4560
)
4661
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
4762
return try await graphqlSubscribe(
@@ -54,19 +69,20 @@ extension GraphQLHandler where Context: WebSocketRequestContext {
5469
).get()
5570
}
5671
)
57-
server.onMessage { message in
58-
logger.trace("GraphQL server received: \(String(message))")
59-
}
60-
server.auth(config.websocket.onWebSocketInit)
72+
try await server.listen(to: messageStream)
6173
case .graphqlWs:
6274
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
63-
let server = GraphQLWS.Server<WebSocketInit, AsyncThrowingStream<GraphQLResult, Error>>(
75+
let server = GraphQLWS.Server<WebSocketInit, WebSocketInitResult, AsyncThrowingStream<GraphQLResult, Error>>(
6476
messenger: messenger,
65-
onExecute: { graphQLRequest in
66-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
77+
onInit: { initPayload in
78+
try await config.websocket.onWebSocketInit(initPayload, context.request, context.requestContext)
79+
},
80+
onExecute: { graphQLRequest, initResult in
81+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
6782
hummingbirdRequest: context.request,
6883
hummingbirdContext: context.requestContext,
69-
graphQLRequest: graphQLRequest
84+
graphQLRequest: graphQLRequest,
85+
websocketInitResult: initResult
7086
)
7187
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
7288
return try await graphql(
@@ -78,11 +94,12 @@ extension GraphQLHandler where Context: WebSocketRequestContext {
7894
operationName: graphQLRequest.operationName
7995
)
8096
},
81-
onSubscribe: { graphQLRequest in
82-
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context>(
97+
onSubscribe: { graphQLRequest, initResult in
98+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<Context, WebSocketInitResult>(
8399
hummingbirdRequest: context.request,
84100
hummingbirdContext: context.requestContext,
85-
graphQLRequest: graphQLRequest
101+
graphQLRequest: graphQLRequest,
102+
websocketInitResult: initResult
86103
)
87104
let graphQLContext = try await computeContext(graphQLContextComputationInputs)
88105
return try await graphqlSubscribe(
@@ -95,12 +112,8 @@ extension GraphQLHandler where Context: WebSocketRequestContext {
95112
).get()
96113
}
97114
)
98-
server.onMessage { message in
99-
logger.trace("GraphQL server received: \(String(message))")
100-
}
101-
server.auth(config.websocket.onWebSocketInit)
115+
try await server.listen(to: messageStream)
102116
}
103-
try await messenger.start()
104117
}
105118

106119
func shouldUpgrade(request: Request) throws -> RouterShouldUpgrade {

Sources/GraphQLHummingbird/WebSocket/WebSocketMessenger.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,22 @@ import NIOCore
66

77
/// Messenger wrapper for WebSockets
88
class WebSocketMessenger: GraphQLTransportWS.Messenger, GraphQLWS.Messenger, @unchecked Sendable {
9-
private let inbound: WebSocketInboundStream
109
private let outbound: WebSocketOutboundWriter
1110
private let logger: Logger
1211

13-
private var onReceive: (String) async throws -> Void = { _ in }
14-
1512
init(
16-
inbound: WebSocketInboundStream,
1713
outbound: WebSocketOutboundWriter,
1814
logger: Logger
1915
) {
20-
self.inbound = inbound
2116
self.outbound = outbound
2217
self.logger = logger
2318
}
2419

25-
/// A blocking method to start listening to the inbound messages and bind them to the onReceive callback
26-
func start() async throws {
27-
// TODO: Make maxSize configurable
28-
for try await message in inbound.messages(maxSize: 1024 * 1024) {
29-
guard case let .text(text) = message else { continue }
30-
do {
31-
try await onReceive(text)
32-
} catch {
33-
try? await self.error("\(error)", code: 4400)
34-
}
35-
}
36-
}
37-
3820
func send<S: Collection>(_ message: S) async throws where S.Element == Character {
3921
logger.trace("GraphQL server sent: \(String(message))")
4022
try await outbound.write(.text(String(message)))
4123
}
4224

43-
func onReceive(callback: @escaping (String) async throws -> Void) {
44-
onReceive = callback
45-
}
46-
4725
func error(_ message: String, code: Int) async throws {
4826
try await outbound.close(.init(codeNumber: code), reason: message)
4927
}

0 commit comments

Comments
 (0)