From 3014ba0daa839ac537a27c12fb98b60e2bcfe636 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 8 Feb 2026 12:24:53 -0700 Subject: [PATCH 1/2] feat!: Centralizes computeContext inputs into one struct This allows non-breaking expansion of supported inputs in the future. We also added GraphQLRequest as an additional piece of data to use in context computation. --- .../Sources/HelloWorld/HelloWorld.swift | 4 +- README.md | 6 +-- .../GraphQLHummingbird/GraphQLHandler.swift | 10 ++++- .../HTTP/GraphQLHandler+HTTP.swift | 18 ++++++-- .../GraphQLHummingbird/Router+graphql.swift | 7 ++- .../GraphQLHandler+handleWebSocket.swift | 44 ++++++++++++++----- .../HTTPStatusCodeGraphQLJSONTests.swift | 14 +++--- .../HTTPStatusCodeJSONTests.swift | 14 +++--- Tests/GraphQLHummingbirdTests/HTTPTests.swift | 24 +++++----- .../WebSocketTests.swift | 6 +-- 10 files changed, 94 insertions(+), 53 deletions(-) diff --git a/Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift b/Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift index 6ccd9c8..c5df9b3 100644 --- a/Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift +++ b/Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift @@ -42,11 +42,11 @@ struct HelloWorld { ) let router = Router(context: HummingbirdContext.self) - router.graphql(schema: schema, config: .init(allowMissingAcceptHeader: true)) { _, _ in + router.graphql(schema: schema, config: .init(allowMissingAcceptHeader: true)) { _ in GraphQLContext() } let webSocketRouter = Router(context: HummingbirdWebSocketContext.self) - webSocketRouter.graphqlWebSocket(schema: schema) { _, _ in + webSocketRouter.graphqlWebSocket(schema: schema) { _ in GraphQLContext() } let app = Application( diff --git a/README.md b/README.md index 3f4594f..e86f5a8 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ struct GraphQLContext: Sendable {} // Create router and register GraphQL let router = Router() -router.graphql(schema: schema) { _, _ in +router.graphql(schema: schema) { _ in return GraphQLContext() } @@ -113,7 +113,7 @@ struct MyWebSocketContext: WebSocketRequestContext, RequestContext { } let router = Router(context: MyContext.self) -router.graphql(schema: schema) { _, _ in +router.graphql(schema: schema) { _ in GraphQLContext() } let webSocketRouter = Router(context: MyWebSocketContext.self) @@ -143,7 +143,7 @@ router.graphql( config: .init( coders: .init(graphQLJSONEncoder: graphQLJSONEncoder) ) -) { _, _ in +) { _ in GraphQLContext() } ``` diff --git a/Sources/GraphQLHummingbird/GraphQLHandler.swift b/Sources/GraphQLHummingbird/GraphQLHandler.swift index dca26e9..03902e0 100644 --- a/Sources/GraphQLHummingbird/GraphQLHandler.swift +++ b/Sources/GraphQLHummingbird/GraphQLHandler.swift @@ -9,5 +9,13 @@ struct GraphQLHandler< let schema: GraphQLSchema let rootValue: any Sendable let config: GraphQLConfig - let computeContext: @Sendable (Request, Context) async throws -> GraphQLContext + let computeContext: @Sendable (GraphQLContextComputationInputs) async throws -> GraphQLContext +} + +public struct GraphQLContextComputationInputs< + Context: RequestContext +>: Sendable { + public let hummingbirdRequest: Request + public let hummingbirdContext: Context + public let graphQLRequest: GraphQLRequest } diff --git a/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift b/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift index 79ac056..a1ea490 100644 --- a/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift +++ b/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift @@ -24,10 +24,15 @@ extension GraphQLHandler { guard operationType != .mutation else { throw HTTPError(.methodNotAllowed, message: "Mutations using GET are disallowed") } - let graphqlContext = try await computeContext(request, context) + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: request, + hummingbirdContext: context, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) let result = await execute( graphQLRequest: graphQLRequest, - context: graphqlContext, + context: graphQLContext, additionalValidationRules: config.additionalValidationRules ) return try encodeResponse(result: result, request: request, context: context) @@ -58,10 +63,15 @@ extension GraphQLHandler { throw HTTPError(.unsupportedMediaType) } - let graphqlContext = try await computeContext(request, context) + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: request, + hummingbirdContext: context, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) let result = await execute( graphQLRequest: graphQLRequest, - context: graphqlContext, + context: graphQLContext, additionalValidationRules: config.additionalValidationRules ) return try encodeResponse(result: result, request: request, context: context) diff --git a/Sources/GraphQLHummingbird/Router+graphql.swift b/Sources/GraphQLHummingbird/Router+graphql.swift index a5815ad..52cb4c0 100644 --- a/Sources/GraphQLHummingbird/Router+graphql.swift +++ b/Sources/GraphQLHummingbird/Router+graphql.swift @@ -20,7 +20,7 @@ public extension RouterMethods { schema: GraphQLSchema, rootValue: any Sendable = (), config: GraphQLConfig = .init(), - computeContext: @Sendable @escaping (Request, Context) async throws -> GraphQLContext + computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> GraphQLContext ) -> Self { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request let handler = GraphQLHandler( @@ -84,7 +84,7 @@ public extension RouterMethods where Context: WebSocketRequestContext { schema: GraphQLSchema, rootValue: any Sendable = (), config: GraphQLConfig = GraphQLConfig(), - computeContext: @Sendable @escaping (Request, Context) async throws -> GraphQLContext + computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> GraphQLContext ) -> Self { let handler = GraphQLHandler( schema: schema, @@ -96,12 +96,11 @@ public extension RouterMethods where Context: WebSocketRequestContext { ws(path, shouldUpgrade: { request, _ in try handler.shouldUpgrade(request: request) }) { inbound, outbound, context in - let graphQLContext = try await computeContext(context.request, context.requestContext) let subProtocol = try handler.negotiateSubProtocol(request: context.request) try await handler.handleWebSocket( inbound: inbound, outbound: outbound, - graphqlContext: graphQLContext, + context: context, subProtocol: subProtocol, logger: context.logger ) diff --git a/Sources/GraphQLHummingbird/WebSocket/GraphQLHandler+handleWebSocket.swift b/Sources/GraphQLHummingbird/WebSocket/GraphQLHandler+handleWebSocket.swift index 5e9d1bd..b83252b 100644 --- a/Sources/GraphQLHummingbird/WebSocket/GraphQLHandler+handleWebSocket.swift +++ b/Sources/GraphQLHummingbird/WebSocket/GraphQLHandler+handleWebSocket.swift @@ -6,11 +6,11 @@ import Hummingbird import HummingbirdWebSocket import Logging -extension GraphQLHandler { +extension GraphQLHandler where Context: WebSocketRequestContext { func handleWebSocket( inbound: WebSocketInboundStream, outbound: WebSocketOutboundWriter, - graphqlContext: GraphQLContext, + context: WebSocketRouterContext, subProtocol: WebSocketSubProtocol, logger: Logger ) async throws { @@ -22,21 +22,33 @@ extension GraphQLHandler { let server = GraphQLTransportWS.Server>( messenger: messenger, onExecute: { graphQLRequest in - try await graphql( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: context.request, + hummingbirdContext: context.requestContext, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) + return try await graphql( schema: self.schema, request: graphQLRequest.query, rootValue: self.rootValue, - context: graphqlContext, + context: graphQLContext, variableValues: graphQLRequest.variables, operationName: graphQLRequest.operationName ) }, onSubscribe: { graphQLRequest in - try await graphqlSubscribe( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: context.request, + hummingbirdContext: context.requestContext, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) + return try await graphqlSubscribe( schema: self.schema, request: graphQLRequest.query, rootValue: self.rootValue, - context: graphqlContext, + context: graphQLContext, variableValues: graphQLRequest.variables, operationName: graphQLRequest.operationName ).get() @@ -51,21 +63,33 @@ extension GraphQLHandler { let server = GraphQLWS.Server>( messenger: messenger, onExecute: { graphQLRequest in - try await graphql( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: context.request, + hummingbirdContext: context.requestContext, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) + return try await graphql( schema: self.schema, request: graphQLRequest.query, rootValue: self.rootValue, - context: graphqlContext, + context: graphQLContext, variableValues: graphQLRequest.variables, operationName: graphQLRequest.operationName ) }, onSubscribe: { graphQLRequest in - try await graphqlSubscribe( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( + hummingbirdRequest: context.request, + hummingbirdContext: context.requestContext, + graphQLRequest: graphQLRequest + ) + let graphQLContext = try await computeContext(graphQLContextComputationInputs) + return try await graphqlSubscribe( schema: self.schema, request: graphQLRequest.query, rootValue: self.rootValue, - context: graphqlContext, + context: graphQLContext, variableValues: graphQLRequest.variables, operationName: graphQLRequest.operationName ).get() diff --git a/Tests/GraphQLHummingbirdTests/HTTPStatusCodeGraphQLJSONTests.swift b/Tests/GraphQLHummingbirdTests/HTTPStatusCodeGraphQLJSONTests.swift index a70322c..e3a58eb 100644 --- a/Tests/GraphQLHummingbirdTests/HTTPStatusCodeGraphQLJSONTests.swift +++ b/Tests/GraphQLHummingbirdTests/HTTPStatusCodeGraphQLJSONTests.swift @@ -16,7 +16,7 @@ struct HTTPStatusCodeGraphQLJSONTests { @Test func parsingFailureGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#json-parsing-failure-1 let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -36,7 +36,7 @@ struct HTTPStatusCodeGraphQLJSONTests { @Test func invalidParametersGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#invalid-parameters-1 let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -56,7 +56,7 @@ struct HTTPStatusCodeGraphQLJSONTests { @Test func documentParsingFailureGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-parsing-failure-1 let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -76,7 +76,7 @@ struct HTTPStatusCodeGraphQLJSONTests { @Test func documentValidationFailureGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-validation-failure-1 let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -97,7 +97,7 @@ struct HTTPStatusCodeGraphQLJSONTests { @Test func operationCannotBeDeterminedGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#operation-cannot-be-determined-1 let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -136,7 +136,7 @@ struct HTTPStatusCodeGraphQLJSONTests { ) ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in EmptyContext() } let app = Application(router: router) @@ -171,7 +171,7 @@ struct HTTPStatusCodeGraphQLJSONTests { ) ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in EmptyContext() } let app = Application(router: router) diff --git a/Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift b/Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift index 4ac15f1..c86c81a 100644 --- a/Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift +++ b/Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift @@ -16,7 +16,7 @@ struct HTTPStatusCodeJSONTests { @Test func parsingFailureGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#json-parsing-failure let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -36,7 +36,7 @@ struct HTTPStatusCodeJSONTests { @Test func invalidParametersGivesBadRequest() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#invalid-parameters let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -56,7 +56,7 @@ struct HTTPStatusCodeJSONTests { @Test func documentValidationFailureGivesOk() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-validation-failure let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -77,7 +77,7 @@ struct HTTPStatusCodeJSONTests { @Test func documentParsingFailureGivesOk() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-parsing-failure let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -116,7 +116,7 @@ struct HTTPStatusCodeJSONTests { ) ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in EmptyContext() } let app = Application(router: router) @@ -138,7 +138,7 @@ struct HTTPStatusCodeJSONTests { @Test func operationCannotBeDeterminedGivesOk() async throws { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#operation-cannot-be-determined let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -171,7 +171,7 @@ struct HTTPStatusCodeJSONTests { ) ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in EmptyContext() } let app = Application(router: router) diff --git a/Tests/GraphQLHummingbirdTests/HTTPTests.swift b/Tests/GraphQLHummingbirdTests/HTTPTests.swift index 2755361..43602ba 100644 --- a/Tests/GraphQLHummingbirdTests/HTTPTests.swift +++ b/Tests/GraphQLHummingbirdTests/HTTPTests.swift @@ -12,7 +12,7 @@ import Testing struct HTTPTests { @Test func query() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -56,7 +56,7 @@ struct HTTPTests { ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in EmptyContext() } let app = Application(router: router) @@ -104,7 +104,7 @@ struct HTTPTests { ) let router = Router() - router.graphql(schema: schema) { _, _ in + router.graphql(schema: schema) { _ in Context(message: "Hello from context!") } let app = Application(router: router) @@ -128,7 +128,7 @@ struct HTTPTests { @Test func jsonAcceptHeader() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -155,7 +155,7 @@ struct HTTPTests { @Test func jsonContentTypeHeader() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -182,7 +182,7 @@ struct HTTPTests { @Test func noAcceptHeader() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -203,7 +203,7 @@ struct HTTPTests { @Test func defaultAcceptHeader() async throws { let router = Router() - router.graphql(schema: helloWorldSchema, config: .init(allowMissingAcceptHeader: true)) { _, _ in + router.graphql(schema: helloWorldSchema, config: .init(allowMissingAcceptHeader: true)) { _ in EmptyContext() } let app = Application(router: router) @@ -229,7 +229,7 @@ struct HTTPTests { @Test func allowGetRequest() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -256,7 +256,7 @@ struct HTTPTests { config: .init( allowGet: false ) - ) { _, _ in + ) { _ in EmptyContext() } let app = Application(router: router) @@ -274,7 +274,7 @@ struct HTTPTests { @Test func graphiql() async throws { let router = Router() - router.graphql(schema: helloWorldSchema) { _, _ in + router.graphql(schema: helloWorldSchema) { _ in EmptyContext() } let app = Application(router: router) @@ -294,7 +294,7 @@ struct HTTPTests { @Test func graphiqlSubscription() async throws { let router = Router() - router.graphql(schema: helloWorldSchema, config: .init(subscriptionProtocols: [.websocket])) { _, _ in + router.graphql(schema: helloWorldSchema, config: .init(subscriptionProtocols: [.websocket])) { _ in EmptyContext() } let app = Application(router: router) @@ -321,7 +321,7 @@ struct HTTPTests { config: .init( coders: .init(graphQLJSONEncoder: graphQLJSONEncoder) ) - ) { _, _ in + ) { _ in EmptyContext() } let app = Application(router: router) diff --git a/Tests/GraphQLHummingbirdTests/WebSocketTests.swift b/Tests/GraphQLHummingbirdTests/WebSocketTests.swift index 52c73fb..2a53fa3 100644 --- a/Tests/GraphQLHummingbirdTests/WebSocketTests.swift +++ b/Tests/GraphQLHummingbirdTests/WebSocketTests.swift @@ -34,7 +34,7 @@ struct WebSocketTests { ) let router = Router(context: TestWebSocketContext.self) - router.graphqlWebSocket(schema: schema) { _, _ in + router.graphqlWebSocket(schema: schema) { _ in EmptyContext() } let app = Application( @@ -109,7 +109,7 @@ struct WebSocketTests { ) let router = Router(context: TestWebSocketContext.self) - router.graphqlWebSocket(schema: schema) { _, _ in + router.graphqlWebSocket(schema: schema) { _ in EmptyContext() } let app = Application( @@ -184,7 +184,7 @@ struct WebSocketTests { ) let router = Router(context: TestWebSocketContext.self) - router.graphqlWebSocket(schema: schema) { _, _ in + router.graphqlWebSocket(schema: schema) { _ in EmptyContext() } let app = Application( From abc91550176f3a26fb54e091bc331fa07ce8e3b1 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 8 Feb 2026 13:38:12 -0700 Subject: [PATCH 2/2] docs: Adds context computation documentation --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index e86f5a8..ecd362c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,21 @@ Response: See the `graphql` function documentation for advanced configuration options. +### Computing GraphQL Context + +The required closure in the `graphql` function is used to compute the `GraphQLContext` object, which is injected into each GraphQL resolver. The `inputs` argument passes in data from the request so that the Context can be created dynamically: + +```swift +router.graphql(schema: schema) { inputs in + return GraphQLContext( + userID: inputs.hummingbirdContext.userID, + logger: inputs.hummingbirdContext.logger, + debug: inputs.hummingbirdRequest.headers[.init("debug")!] != nil, + operationName: inputs.graphQLRequest.operationName + ) +} +``` + ### WebSockets Subscription support via WebSockets can be enabled by calling the `graphqlWebSocket` function on a `Router` whose context conforms to `WebSocketRequestContext`, from the `HummingbirdWebSocket` package: