Documentation
Web Frameworks
Vapor

Vapor

Pioneer will have a built-in first-party integration with Vapor (opens in a new tab). This aims to make developing with Pioneer faster by not having to worry about creating integrations for the most common option for a web framework in Swift.

This integration added a couple additional benefits.

import Pioneer
import Vapor
 
let app = try Application(.detect())
 
let server = Pioneer(
    schema: schema,
    resolver: Resolver(),
    websocketProtocol: .graphqlWs,
    introspection: true,
    playground: .sandbox
)
 
app.middleware.use(
    server.vaporMiddleware()
)

Context

HTTP-based Context

Pioneer provide a similar solution to @apollo/server/express4 for building context using the raw HTTP requests and responses. It provide both in the context builder that needed to be provided when create the middleware.

⚡️

This request and response will be request-specific / different for each GraphQL HTTP request.

import Pioneer
import Vapor
 
let app = try Application(.detect())
 
let server = Pioneer(
    schema: schema,
    resolver: Resolver(),
    websocketProtocol: .graphqlWs,
    introspection: true,
    playground: .sandbox
)
 
app.middleware.use(
    server.vaporMiddleware(
        context: { (req: Request, res: Response) in
            ...
        }
    )
)

Context builder can be asynchronous and/or throwing.

If the context builder throws, it will prevent the operation from being executed

app.middleware.use(
    server.vaporMiddleware(
        context: { req, _ async throws in
            guard let user = await auth(req) else {
                throw Abort(.badRequest) // Response status code of 400
            }
            return Context(req: req)
        }
    )
)

Request (HTTP)

The request given is directly from Vapor (opens in a new tab), so you can use any method you would use in a regular Vapor (opens in a new tab) application to get any values from it.

Getting a cookie example
func someCookie(ctx: Context, _: NoArguments) async -> String {
    return ctx.req.cookies["some-key"]
}

Response

The response object is already provided in the context builder that is going to be the one used to respond to the request.

You don't need return one, and instead just mutate its properties.

Setting a cookie example
func users(ctx: Context, _: NoArguments) async -> [User] {
    ctx.response.cookies["refresh-token"] = /* refresh token */
    ctx.response.cookies["access-token"] = /* access token */
    return await getUsers()
}

Websocket-based Context

Vapor (opens in a new tab) integration also allow seperate context builder which is similar to what you can provide to the context (opens in a new tab) property in graphql-ws (opens in a new tab) where you are given the Request, Payload, and GraphQLRequest.

⚡️

WebSocket context builder is optional.

Pioneer's Vapor (opens in a new tab) integration will try to use the HTTP context builder for WebSocket by providing all the relevant information into the Request.

import Pioneer
import Vapor
 
let app = try Application(.detect())
 
let server = Pioneer(
    schema: schema,
    resolver: Resolver(),
    websocketProtocol: .graphqlWs,
    introspection: true,
    playground: .sandbox
)
 
app.middleware.use(
    server.vaporMiddleware(
        context: { (req: Request, res: Response) in
            ...
        },
        websocketContext: { (req: Request, payload: Payload, gql: GraphQLRequest) in
            ...
        }
    )
)

Request (WS)

The request given is directly from Vapor when upgrading to websocket, so you can use any method you would use in a regular Vapor application to get any values from it.

⚠️

Switching Protocol Request
This request object will be the same for each websocket connection and will not change unless the new connection is made.
It will also not have any custom headers and the operation specific graphql query which is different from request given in HTTP.

Getting Fluent DB or EventLoop
struct Resolver {
    func something(ctx: Context, _: NoArguments) async -> [User] {
        return User.query(on: ctx.req.db).all()
    }
}
Changes to Request for shared context builder

This is only for using 1 shared context builder, and not providing a separate WebSocket context builder.

The custom request will similar to the request used to upgrade to websocket but will have:

  • The headers taken from "header"/"headers" value from the Payload or all the entirety of Payload
  • The query parameters taken from "query"/"queries"/"queryParams"/"queryParameters" value from the Payload
  • The body from the GraphQLRequest

Payload

The connection params is given during websocket initialization from payload as part of ConnectionInit message (opens in a new tab) inside an established WebSocket connection.

The given payload is not statically typed. However, you can decode it to a custom payload type using the .decode(_) method

Getting some values
struct AuthPayload: Decodable {
    var authorization: String
}
 
func someHeader(ctx: Context, _: NoArguments) async -> String? {
    guard let payload = ctx.payload.decode(AuthPayload.self) else { ... }
    return payload.authorization
}

GraphQLRequest

This is operation specific graphql request / query given an operation is being executed.

Getting operation type
func someHeader(ctx: Context, _: NoArguments) throws -> String? {
    switch try ctx.gql.operationType() {
    case .subscription:
        ...
    case .query:
        ...
    case .mutation:
        ...
    }
}

WebSocket Guard

There might be times where you want to authorize any incoming WebSocket connection before any operation done, and thus before the context builder is executed.

Pioneer's Vapor (opens in a new tab) integration provide a way to run custom code during the GraphQL over WebSocket initialisation phase that can deny a WebSocket connection by throwing an error.

app.middleware.use(
	server.vaporMiddleware(
		context: { req, res in
			...
		},
		websocketContext: { req, payload, gql in
			...
		},
		websocketGuard: { req, payload in 
			...
		}
	)
)

Handlers

Pioneer's Vapor (opens in a new tab) also exposes the HTTP handlers for GraphQL over HTTP operations, GraphQL over WebSocket upgrade, and GraphQL IDE hosting.

This allow opting out of the middleware for integrating Pioneer and Vapor (opens in a new tab), by manually setting this handlers on routes.

GraphQL IDE hosting

.ideHandler (opens in a new tab) will serve incoming request with the configured GraphQL IDE.

app.group("graphql") { group in
    group.get { req in
        server.ideHandler(req: req)
    }
}

GraphQL over HTTP operations

.httpHandler (opens in a new tab) will execute a GraphQL operation and return a well-formatted response.

app.group("graphql") { group in
    group.post { req in
        try await server.httpHandler(
            req: req, 
            context: { req, res in 
                ...
            }
        )
    }
}

GraphQL over WebSocket upgrade

.webSocketHandler (opens in a new tab) will upgrade incoming request into a WebSocket connection and start the process of GraphQL over Websocket.

app.group("graphql") { group in
    group.get("ws") { req in
        try await server.webSocketHandler(
            req: req,
            context: { req, payload, gql in
                ...
            },
            guard: { req, payload in
                ...
            }
        )
    }
}

Additional benefits

The Vapor (opens in a new tab) integration include other benefits such as:

  • Includes all security measurements done by Pioneer automatically (i.e. CSRF Prevention)
  • Automatically operation check for HTTP methods using the given HTTPStrategy
  • Extensions for CORSMiddleware.Configuration for allowing Cloud based GraphQL IDEs
Last updated on December 28, 2022