Documentation
Web Frameworks
Integration

Integrations

Open-Source Integrations

Exisiting first-party of community maintained integrations for Pioneer:

Web FrameworkIntegration Package
Vapor (opens in a new tab)Pioneer
Hummingbird (opens in a new tab)Pioneer Hummingbird* (opens in a new tab)

*: Under heavy development and not production-ready yet

Building integrations

This section is for authors of web frameworks integrations. Before building a new integration, it's recommended seeing if there's an integration for your framework of choice that suits your needs

Implementing GraphQL over HTTP

First, the HTTP layer. Pioneer provide a method .executeHTTPGraphQLRequest (opens in a new tab) which is the base layer of an GraphQL would look like HTTP handler.

All that is missing to use that method is translating the web-framework native request object into HTTPGraphQLRequest (opens in a new tab).

Mapping into HTTPGraphQLRequest (opens in a new tab)

HTTPGraphQLRequest (opens in a new tab) only require 3 properties: the GraphQLRequest object, the HTTP headers, and the HTTP method.

struct HTTPGraphQLRequest {
    var request: GraphQLRequest
    var headers: HTTPHeaders
    var method: HTTPMethod
}

The important part is parsing into GraphQLRequest (opens in a new tab). This can be done by making sure the web-framework request object conforms to the GraphQLRequestConvertible (opens in a new tab) protocol.

After that, the GraphQLRequest (opens in a new tab) can be accessed from the property .graphql (opens in a new tab).

Example
import class WebFramework.Request
 
extension Request: GraphQLRequestConvertible {
    public func body<T>(_ decodable: T.Type) throws -> T where T: Decodable {
        try JSONDecoder().decode(decodable, from: body)
    }
 
    public func searchParams<T>(_ decodable: T.Type, at: String) -> T? where T: Decodable {
        search[at]?.removingPercentEncoding
            .flatMap { $0.data(using: .utf8) }
            .flatMap { try? JSONDecoder().decode(decodable, from: $0) }
    }
 
    public var isAcceptingGraphQLResponse: Bool {
        headers[.accept].contains(HTTPGraphQLRequest.mediaType)
    }
}

Getting the context

It's important that the context should be computed / derived for each request. By convention, it's best to allow user of the integration to compute the context from the request and the response object of the web-framework.

If the compute function is allowed to be asynchronous, make sure to make it Sendable conformance by adding the @Sendable function wrapper.

Example
import class WebFramework.Request
import class WebFramework.Response
import struct Pioneer.Pioneer
 
extension Pioneer {
    typealias WebFrameworkHTTPContext = @Sendable (Request, Response) async throws -> Context
}

Executing and using HTTPGraphQLResponse (opens in a new tab)

Once, there is a way to retreive HTTPGraphQLRequest (opens in a new tab) and the context. All is needed is to execute the request and mapped the HTTPGraphQLRequest (opens in a new tab) into the web-framework response object.

struct HTTPGraphQLResponse {
    var result: GraphQLResult
    var status: HTTPResponseStatus
}

The property .graphql may throw a GraphQLViolation (opens in a new tab) error. This error should be caught, the its message and status value should be use in the response to comply with the GraphQL over HTTP specification.

Example
import class WebFramework.Request
import class WebFramework.Response
import struct Pioneer.Pioneer
import struct GraphQL.GraphQLJSONEncoder
 
extension Pioneer {
    public func httpHandler(req: Request, context: @escaping WebFrameworkHTTPContext) async throws -> Response {
        do {
            // Parsing HTTPGraphQLRequest and Context
            guard let httpreq = req.graphql else {
                return Response(status: .badRequest)
            }
            let res = Response()
            let context = try await context(req, res)
 
            // Executing into GraphQLResult
            let httpRes = await executeHTTPGraphQLRequest(for: httpreq, with: context, using: req.eventLoop)
            res.body = try GraphQLJSONEncoder().encode(httpres.result)
            res.status = httpRes.status
 
            return res
        } catch let e as GraphQLViolation {
            let body = try GraphQLJSONEncoder().encode(GraphQLResult(data: nil, errors: [.init(e.message)]))
            return Response(status: e.status(req.isAcceptingGraphQLResponse), body: body)
        } catch {
            // Format error caught into GraphQLResult
            let body = try GraphQLJSONEncoder().encode(GraphQLResult(data: nil, errors: [.init(error)]))
            return Response(status: .internalServerError, body: body)
        }
    }
}

Implementing GraphQL IDE

This is part is relatively simple, send back the web-framework response that contains the HTML for the given IDE or a redirect if the IDE was set to be a redirect.

The HTML for each type of IDE are available as computed properties of Pioneer. The URL for the Cloud IDEs are accessible property.

All that is needed is to serve this HTML and redirect if the IDE option is a redirect using the URL given.

Example
import class WebFramework.Request
import class WebFramework.Response
import struct Pioneer.Pioneer
 
extension Pioneer {
    func ideHandler(req: Request) -> Response {
        switch (playground) {
            case .sandbox:
                return serve(html: embeddedSandboxHtml)
            case .graphiql:
                return serve(html: graphiqlHtml)
            case .playground:
                return serve(html: playgroundHtml)
            case .redirect(to: let cloud):
                return Response(status: .permanentRedirect, redirect: cloud.url)
        }
    }
 
    func serve(html: String) -> Response {
        Response(
            status: .ok,
            headers: ["Content-Type": "text/html"],
            body: html.data(using: .utf8)
        )
    }
}

Implemeting GraphQL over WebSocket

Implementing the WebSocket layer can be tricky to do. Pioneer already provide all the callbacks need to setup GraphQL over WebSocket, the only thing missing is to connect that to the WebSocket portion of the web-framework.

Upgrading HTTP Request into WebSocket

It is important that the desired web-framework can be used to perform upgrade to WebSocket from a regular HTTP requests.

The only thing needed to be done before the upgrade is done, is to check whether the Sec-WebSocket-Protocol header value is matching the WebSocket protocol name

Example
import class WebFramework.Request
import struct WebFramework.BadRequestError
import struct Pioneer.Pioneer
 
extension Pioneer {
    func shouldUpgrade(req: Request) async throws -> HTTPHeaders {
        guard let req.headers[.secWebSocketProtocol].first(where: websocketProtocol.isValid) else {
            throw BadRequestError()
        }
 
        return req.headers
    }
}

Context and Guard

Before proceeding, similarly to HTTP, context is a crutial part of the GraphQL operation. By convention for WebSocket, it's best to allow user of the integration to compute the context from the request, the initial payload, and the GraphQL operation itself.

The only other addition is WebSocket guard. It is also desirable for the user to be able to perform action just after the initialisation process using the request and the initial payload.

Example
import class WebFramework.Request
import struct Pioneer.Pioneer
import struct Pioneer.GraphQLRequest
import enum Pioneer.Payload
 
extension Pioneer {
    typealias WebFrameworkWebSocketContext = @Sendable (Request, Payload, GraphQLRequest) async throws -> Context
    typealias WebFrameworkWebSocketGuard = @Sendable (Request, Payload) async throws -> Void
}

Making WebSocket WebSocketable

In order for Pioneer to use the web-framework specific implementation of WebSocket. The web-framework WebSocket object must conforms the WebSocketable (opens in a new tab) protocol.

Example
import enum NIOWebSocket.WebSocketErrorCode
import class WebFramework.WebSocket
 
extension WebSocket: WebSocketable {
    public func out<S>(_ msg: S) where S: Collection, S.Element == Character {
        send(msg)
    }
 
    public func terminate(code: WebSocketErrorCode) async throws {
        try await close(code: code)
    }
}

Setting up GraphQL over WebSocket

After the upgrade is done, there's only a few things to do:

Example
import class WebFramework.Request
import class WebFramework.Response
import class WebFramework.WebSocket
import struct Pioneer.Pioneer
 
extension Pioneer {
    func wsHandler(
        req: Request,
        context: @escaping WebFrameworkWebSocketContext,
        guard: @escaping WebFrameworkWebSocketGuard
    ) async throws -> Response {
        req.upgradeToWebSocket(shouldUpgrade: shouldUpgrade(req:)) { req, ws
            onUpgrade(req: req, ws: ws, context: context, guard: `guard`)
        }
    }
 
 
    func onUpgrade(
        req: Request,
        ws: WebSocket,
        context: @escaping WebFrameworkWebSocketContext,
        guard: @escaping WebFrameworkWebSocketGuard
    ) -> Void {
        let cid = UUID()
 
        // Keep alive and timeout task
        let keepAlive = keepAlive(using: ws)
        let timeout = timeout(using: ws, keepAlive: keepAlive)
 
        // Consuming incoming message
        let receiving = Task {
            let stream = AsyncStream(String.self) { con in
                ws.onMessage(con.yield)
 
                con.onTermination = { @Sendable _ in
                    guard ws.isClosed else { return }
                    _ = ws.close()
                }
            }
 
            for await message in stream {
                await receiveMessage(
                    cid: cid, io: ws,
                    keepAlive: keepAlive,
                    timeout: timeout,
                    ev: req.eventLoop,
                    txt: message,
                    context: {
                        try await context(req, $0, $1)
                    },
                    check: {
                        try await `guard`(req, $0)
                    }
                )
            }
        }
 
        // Closing task
        Task {
            try await ws.onClose.get()
            receiving.cancel()
            disposeClient(cid: cid, keepAlive: keepAlive, timeout: timeout)
        }
    }
}
Last updated on October 28, 2023