Skip to content

Commit 3aa03a1

Browse files
Merge pull request #6 from GraphQLSwift/feat/graphqlws-v1
Includes WebSocketInitResult in context computation inputs
2 parents e840a9f + e3a1a13 commit 3aa03a1

11 files changed

Lines changed: 194 additions & 73 deletions

Examples/HelloWorld/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.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
@@ -15,8 +15,8 @@ let package = Package(
1515
],
1616
dependencies: [
1717
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.0.0"),
18-
.package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "0.2.1"),
19-
.package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "0.2.1"),
18+
.package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "1.0.0"),
19+
.package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "1.0.0"),
2020
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
2121
],
2222
targets: [

Sources/GraphQLVapor/GraphQLConfig.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import GraphQL
2+
import Vapor
23

34
/// Configuration options for GraphQLVapor
4-
public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Sendable {
5+
public struct GraphQLConfig<
6+
WebSocketInit: Equatable & Codable & Sendable,
7+
WebSocketInitResult: Sendable
8+
>: Sendable {
59
let allowGet: Bool
610
let allowMissingAcceptHeader: Bool
711
let ide: IDE
@@ -24,7 +28,7 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
2428
subscriptionProtocols: Set<SubscriptionProtocol> = [],
2529
websocket: WebSocket = .init(
2630
// Including this strongly-typed argument is required to avoid compiler failures on Swift 6.2.3.
27-
onWebsocketInit: { (_: EmptyWebsocketInit) in }
31+
onWebSocketInit: { (_: EmptyWebSocketInit, _: Request) in }
2832
),
2933
additionalValidationRules: [@Sendable (ValidationContext) -> Visitor] = []
3034
) {
@@ -67,16 +71,16 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
6771
}
6872

6973
public struct WebSocket: Sendable {
70-
let onWebsocketInit: @Sendable (WebSocketInit) async throws -> Void
74+
let onWebSocketInit: @Sendable (WebSocketInit, Request) async throws -> WebSocketInitResult
7175

7276
/// GraphQL over WebSocket configuration
73-
/// - Parameter onWebsocketInit: A custom callback run during `connection_init` resolution that allows
77+
/// - Parameter onWebSocketInit: A custom callback run during `connection_init` resolution that allows
7478
/// authorization using the `payload` field of the `connection_init` message.
7579
/// Throw from this closure to indicate that authorization has failed.
7680
public init(
77-
onWebsocketInit: @Sendable @escaping (WebSocketInit) async throws -> Void = { (_: EmptyWebsocketInit) in }
81+
onWebSocketInit: @Sendable @escaping (WebSocketInit, Request) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit, _: Request) in }
7882
) {
79-
self.onWebsocketInit = onWebsocketInit
83+
self.onWebSocketInit = onWebSocketInit
8084
}
8185
}
8286
}

Sources/GraphQLVapor/GraphQLHandler.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import Vapor
33

44
struct GraphQLHandler<
55
Context: Sendable,
6-
WebSocketInit: Equatable & Codable & Sendable
6+
WebSocketInit: Equatable & Codable & Sendable,
7+
WebSocketInitResult: Sendable
78
>: Sendable {
89
let schema: GraphQLSchema
910
let rootValue: any Sendable
10-
let config: GraphQLConfig<WebSocketInit>
11-
let computeContext: @Sendable (GraphQLContextComputationInputs) async throws -> Context
11+
let config: GraphQLConfig<WebSocketInit, WebSocketInitResult>
12+
let computeContext: @Sendable (GraphQLContextComputationInputs<WebSocketInitResult>) async throws -> Context
1213
}

Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ extension GraphQLHandler {
1515
guard operationType != .mutation else {
1616
throw Abort(.methodNotAllowed, reason: "Mutations using GET are disallowed")
1717
}
18-
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
18+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<WebSocketInitResult>(
1919
vaporRequest: request,
20-
graphQLRequest: graphQLRequest
20+
graphQLRequest: graphQLRequest,
21+
websocketInitResult: nil
2122
)
2223
let context = try await computeContext(graphQLContextComputationInputs)
2324
let result = await execute(
@@ -34,9 +35,10 @@ extension GraphQLHandler {
3435
throw Abort(.unsupportedMediaType, reason: "Missing `Content-Type` header")
3536
}
3637
let graphQLRequest = try request.content.decode(GraphQLRequest.self)
37-
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
38+
let graphQLContextComputationInputs = GraphQLContextComputationInputs<WebSocketInitResult>(
3839
vaporRequest: request,
39-
graphQLRequest: graphQLRequest
40+
graphQLRequest: graphQLRequest,
41+
websocketInitResult: nil
4042
)
4143
let context = try await computeContext(graphQLContextComputationInputs)
4244
let result = await execute(

Sources/GraphQLVapor/RoutesBuilder+graphql.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ public extension RoutesBuilder {
2020
/// - computeContext: A closure used to compute the GraphQL context from incoming requests. This must be provided.
2121
func graphql<
2222
Context: Sendable,
23-
WebSocketInit: Equatable & Codable & Sendable
23+
WebSocketInit: Equatable & Codable & Sendable,
24+
WebSocketInitResult: Sendable
2425
>(
2526
_ path: [PathComponent] = ["graphql"],
2627
schema: GraphQLSchema,
2728
rootValue: any Sendable = (),
28-
config: GraphQLConfig<WebSocketInit> = GraphQLConfig<EmptyWebsocketInit>(),
29-
computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> Context
29+
config: GraphQLConfig<WebSocketInit, WebSocketInitResult> = GraphQLConfig<EmptyWebSocketInit, Void>(),
30+
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<WebSocketInitResult>) async throws -> Context
3031
) {
3132
ContentConfiguration.global.use(encoder: GraphQLJSONEncoder(), for: .jsonGraphQL)
3233
ContentConfiguration.global.use(decoder: JSONDecoder(), for: .jsonGraphQL)
3334

3435
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request
35-
let handler = GraphQLHandler<Context, WebSocketInit>(schema: schema, rootValue: rootValue, config: config, computeContext: computeContext)
36+
let handler = GraphQLHandler<Context, WebSocketInit, WebSocketInitResult>(schema: schema, rootValue: rootValue, config: config, computeContext: computeContext)
3637
get(path) { request in
3738
// WebSocket handling
3839
if
@@ -68,7 +69,17 @@ public extension RoutesBuilder {
6869
}
6970
}
7071

71-
public struct GraphQLContextComputationInputs: Sendable {
72+
/// Request metadata that can be used to construct a GraphQL context
73+
public struct GraphQLContextComputationInputs<
74+
WebSocketInitResult: Sendable
75+
>: Sendable {
76+
/// The Vapor request that initiated the GraphQL request. In WebSockets, this is the upgrade GET request.
7277
public let vaporRequest: Request
78+
79+
/// The decoded GraphQL request, including the raw query, variables, and more
7380
public let graphQLRequest: GraphQLRequest
81+
82+
/// The result of the WebSocket's initialization closure. This can be used to customize GraphQL context creation based on the init
83+
/// message metadata as opposed to only the upgrade request. In non-WebSocket contexts, this is nil.
84+
public let websocketInitResult: WebSocketInitResult?
7485
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
public struct EmptyWebsocketInit: Equatable, Codable, Sendable {}
1+
public struct EmptyWebSocketInit: Equatable, Codable, Sendable {}

Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,34 @@ extension GraphQLHandler {
1818
}
1919
},
2020
onUpgrade: { websocket in
21+
let messageStream = AsyncThrowingStream<String, Error> { continuation in
22+
websocket.onText { _, text in
23+
continuation.yield(text)
24+
}
25+
websocket.onClose.whenComplete { result in
26+
switch result {
27+
case .success:
28+
continuation.finish()
29+
case let .failure(error):
30+
continuation.finish(throwing: error)
31+
}
32+
}
33+
}
34+
2135
let messenger = WebSocketMessenger(websocket: websocket)
2236
switch subProtocol {
2337
case .graphqlTransportWs:
2438
// https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
25-
let server = GraphQLTransportWS.Server<WebSocketInit, AsyncThrowingStream<GraphQLResult, Error>>(
39+
let server = GraphQLTransportWS.Server<WebSocketInit, WebSocketInitResult, AsyncThrowingStream<GraphQLResult, Error>>(
2640
messenger: messenger,
27-
onExecute: { graphQLRequest in
41+
onInit: { initPayload in
42+
try await config.websocket.onWebSocketInit(initPayload, request)
43+
},
44+
onExecute: { graphQLRequest, initResult in
2845
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
2946
vaporRequest: request,
30-
graphQLRequest: graphQLRequest
47+
graphQLRequest: graphQLRequest,
48+
websocketInitResult: initResult
3149
)
3250
let context = try await computeContext(graphQLContextComputationInputs)
3351
return try await graphql(
@@ -39,10 +57,11 @@ extension GraphQLHandler {
3957
operationName: graphQLRequest.operationName
4058
)
4159
},
42-
onSubscribe: { graphQLRequest in
60+
onSubscribe: { graphQLRequest, initResult in
4361
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
4462
vaporRequest: request,
45-
graphQLRequest: graphQLRequest
63+
graphQLRequest: graphQLRequest,
64+
websocketInitResult: initResult
4665
)
4766
let context = try await computeContext(graphQLContextComputationInputs)
4867
return try await graphqlSubscribe(
@@ -55,15 +74,22 @@ extension GraphQLHandler {
5574
).get()
5675
}
5776
)
58-
server.auth(config.websocket.onWebsocketInit)
77+
Task {
78+
// This task completes upon websocket closure
79+
try await server.listen(to: messageStream)
80+
}
5981
case .graphqlWs:
6082
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
61-
let server = GraphQLWS.Server<WebSocketInit, AsyncThrowingStream<GraphQLResult, Error>>(
83+
let server = GraphQLWS.Server<WebSocketInit, WebSocketInitResult, AsyncThrowingStream<GraphQLResult, Error>>(
6284
messenger: messenger,
63-
onExecute: { graphQLRequest in
85+
onInit: { initPayload in
86+
try await config.websocket.onWebSocketInit(initPayload, request)
87+
},
88+
onExecute: { graphQLRequest, initResult in
6489
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
6590
vaporRequest: request,
66-
graphQLRequest: graphQLRequest
91+
graphQLRequest: graphQLRequest,
92+
websocketInitResult: initResult
6793
)
6894
let context = try await computeContext(graphQLContextComputationInputs)
6995
return try await graphql(
@@ -75,10 +101,11 @@ extension GraphQLHandler {
75101
operationName: graphQLRequest.operationName
76102
)
77103
},
78-
onSubscribe: { graphQLRequest in
104+
onSubscribe: { graphQLRequest, initResult in
79105
let graphQLContextComputationInputs = GraphQLContextComputationInputs(
80106
vaporRequest: request,
81-
graphQLRequest: graphQLRequest
107+
graphQLRequest: graphQLRequest,
108+
websocketInitResult: initResult
82109
)
83110
let context = try await computeContext(graphQLContextComputationInputs)
84111
return try await graphqlSubscribe(
@@ -91,7 +118,10 @@ extension GraphQLHandler {
91118
).get()
92119
}
93120
)
94-
server.auth(config.websocket.onWebsocketInit)
121+
Task {
122+
// This task completes upon websocket closure
123+
try await server.listen(to: messageStream)
124+
}
95125
}
96126
}
97127
)

Sources/GraphQLVapor/WebSocket/WebSocketMessenger.swift

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,19 @@ import GraphQLWS
33
import WebSocketKit
44

55
/// Messenger wrapper for WebSockets
6-
class WebSocketMessenger: GraphQLTransportWS.Messenger, GraphQLWS.Messenger, @unchecked Sendable {
7-
private weak var websocket: WebSocket?
8-
private var onReceive: (String) async throws -> Void = { _ in }
9-
10-
init(websocket: WebSocket) {
11-
self.websocket = websocket
12-
websocket.onText { _, message in
13-
// We must include self here, without a weak reference to prevent it from falling
14-
// out of scope while the websocket is still alive
15-
do {
16-
try await self.onReceive(message)
17-
} catch {
18-
try? await self.error("\(error)", code: 4400)
19-
}
20-
}
21-
websocket.onClose.whenComplete { [weak self] _ in
22-
guard let self = self else {
23-
return
24-
}
25-
self.onReceive { _ in }
26-
websocket.onText { _, _ in }
27-
}
28-
}
6+
struct WebSocketMessenger: GraphQLTransportWS.Messenger, GraphQLWS.Messenger {
7+
let websocket: WebSocket
298

309
func send<S: Collection>(_ message: S) async throws where S.Element == Character {
31-
guard let websocket = websocket else { return }
3210
try await websocket.send(message)
3311
}
3412

35-
func onReceive(callback: @escaping (String) async throws -> Void) {
36-
onReceive = callback
37-
}
38-
3913
func error(_ message: String, code: Int) async throws {
40-
guard let websocket = websocket else { return }
4114
try await websocket.send("\(code): \(message)")
4215
try await websocket.close(code: .init(codeNumber: code))
4316
}
4417

4518
func close() async throws {
46-
guard let websocket = websocket else { return }
4719
try await websocket.close()
4820
}
4921
}

0 commit comments

Comments
 (0)