Skip to content

Commit

Permalink
chore: Cleanup code and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
d-exclaimation committed Dec 30, 2022
1 parent d37095b commit b466791
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 90 deletions.
35 changes: 19 additions & 16 deletions Documentation/pages/docs/features/graphql-middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ Resolver-based middleware is still a work in progress.
It is currently only supported as extensions to [Graphiti](https://github.com/GraphQLSwift/Graphiti), and only available in `v1.2.0-beta.1` or higher.
</Callout>

Middleware are useful reusable code that can be easily attached to resolvers.
By using middleware we can extract the commonly used code from our resolvers and then declaratively attach it whenever it's needed.
Middlewares are useful reusable code that can be easily attached to resolvers.
It allows you to perform operations on incoming operations before they get to the field resolver and on outgoing responses before they are sent back.

## Creating a middleware

GraphQL middleware is a function that takes 2 arguments:
1. Resolver parameters - _the same as resolvers (root, args, context)_
2. Next function - _used to control the execution of the next middleware and the resolver to which it is attached_
1. Resolver parameters - _All information given to the resolver (root, args, context)_
2. Next function - _Function to control the execution of the next middleware and the resolver to which it is attached_

```swift {5} showLineNumbers copy filename="Specific resolver middleware"
public func MinZeroResult(
params: ResolverParameters<Resolver, Context, NoArguments>,
next: @escaping () -> Int
) async throws -> Int {
try await min(next(), 0)
}
```

```swift {3,5-6} showLineNumbers copy
```swift {3,5-6} showLineNumbers copy filename="Reusable generic middleware"
public func Trace<Root, Args, Returned>() -> GraphQLMiddleware<Root, Context, Args, Returned> {
return { params, next in
let before = Date()
Expand Down Expand Up @@ -65,20 +74,14 @@ public func Cached<Root, Returned: RESPValueConvertible>(

Attaching middlewares can be done through the `use` parameter from `Field`. The order of the middleware is also important.

```swift {4-5,12-13} showLineNumbers copy
```swift {3,8} showLineNumbers copy
Query {
Field("books",
at: Resolver.books,
// Trace -> Cached -> Resolver -> Cached -> Trace
use: [Trace(), Cached(key: "query:books")]
)
// Trace -> Cached -> Resolver
Field("books", at: Resolver.books, use: [Trace(), Cached(key: "query:books")])
}

Mutation {
Field("createBook"
at: Resolver.createBook,
// Auth -> Trace -> Resolver -> Trace -> Auth
use: [Auth(), Trace()]
)
// Auth -> Trace -> Resolver
Field("createBook", at: Resolver.createBook, use: [Auth(), Trace()])
}
```
2 changes: 1 addition & 1 deletion Sources/Pioneer/GraphQL/BuiltinTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public struct ID: Codable, ExpressibleByStringLiteral, CustomStringConvertible,
}

/// Conversion Error for the ID scalar
public struct ConversionError: Error {
public struct ConversionError: Error, @unchecked Sendable {
/// The ID in question as string
public var id: String

Expand Down
80 changes: 20 additions & 60 deletions Sources/Pioneer/GraphQL/Extensions/Field+Middleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,11 @@ public extension Field where FieldType: Encodable {
use middlewares: [GraphQLMiddleware<ObjectType, Context, Arguments, FieldType>],
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
) {
let resolve: ConcurrentResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments in
let info = ResolverParameters(root: type, context: context, args: arguments)
let result = middlewares
.reversed()
.reduce({ () async throws -> FieldType in
try function(type)(context, arguments)
}) { acc, middleware in
{ () async throws -> FieldType in
try await middleware(info, acc)
}
}
return try await result()
}
}
self.init(name: name, arguments: [argument()], concurrentResolve: resolve)
self.init(
name: name,
arguments: [argument()],
concurrentResolve: buildResolver(from: function, using: middlewares)
)
}

convenience init(
Expand All @@ -43,22 +32,11 @@ public extension Field where FieldType: Encodable {
@ArgumentComponentBuilder<Arguments> _ arguments: ()
-> [ArgumentComponent<Arguments>] = { [] }
) {
let resolve: ConcurrentResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments in
let info = ResolverParameters(root: type, context: context, args: arguments)
let result = middlewares
.reversed()
.reduce({ () async throws -> FieldType in
try function(type)(context, arguments)
}) { acc, middleware in
{ () async throws -> FieldType in
try await middleware(info, acc)
}
}
return try await result()
}
}
self.init(name: name, arguments: arguments(), concurrentResolve: resolve)
self.init(
name: name,
arguments: arguments(),
concurrentResolve: buildResolver(from: function, using: middlewares)
)
}

convenience init(
Expand All @@ -67,20 +45,11 @@ public extension Field where FieldType: Encodable {
use middlewares: [GraphQLMiddleware<ObjectType, Context, Arguments, FieldType>],
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
) {
let resolve: ConcurrentResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments in
let info = ResolverParameters(root: type, context: context, args: arguments)
let result = middlewares
.reversed()
.reduce({ try await function(type)(context, arguments) }) { acc, middleware in
{ () async throws -> FieldType in
try await middleware(info, acc)
}
}
return try await result()
}
}
self.init(name: name, arguments: [argument()], concurrentResolve: resolve)
self.init(
name: name,
arguments: [argument()],
concurrentResolve: buildResolver(from: function, using: middlewares)
)
}

convenience init(
Expand All @@ -90,19 +59,10 @@ public extension Field where FieldType: Encodable {
@ArgumentComponentBuilder<Arguments> _ arguments: ()
-> [ArgumentComponent<Arguments>] = { [] }
) {
let resolve: ConcurrentResolve<ObjectType, Context, Arguments, FieldType> = { type in
{ context, arguments in
let info = ResolverParameters(root: type, context: context, args: arguments)
let result = middlewares
.reversed()
.reduce({ try await function(type)(context, arguments) }) { acc, middleware in
{ () async throws -> FieldType in
try await middleware(info, acc)
}
}
return try await result()
}
}
self.init(name: name, arguments: arguments(), concurrentResolve: resolve)
self.init(
name: name,
arguments: arguments(),
concurrentResolve: buildResolver(from: function, using: middlewares)
)
}
}
4 changes: 2 additions & 2 deletions Sources/Pioneer/GraphQL/GraphQLMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import GraphQL

/// GraphQL Websocket Message according to all sub-protocol
public struct GraphQLMessage: Codable {
public struct GraphQLMessage: Codable, @unchecked Sendable {
/// Operation based ID if any
public var id: String?
/// Message type specified to allow differentiation
Expand Down Expand Up @@ -44,7 +44,7 @@ public struct GraphQLMessage: Codable {
}

/// Variant type to escape constraint on payload, use only for cases where certain payload break the object spec
public struct Variance: Codable {
public struct Variance: Codable, @unchecked Sendable {
public var id: String?
public var type: String
public var payload: Map?
Expand Down
51 changes: 51 additions & 0 deletions Sources/Pioneer/GraphQL/GraphQLMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// Created by d-exclaimation on 17:38.
//

import typealias Graphiti.ConcurrentResolve
import typealias Graphiti.SyncResolve

/// A struct to group of all parameters for a resolvers
public struct ResolverParameters<Root, Context, Args> {
/// The root element
Expand All @@ -25,3 +28,51 @@ public typealias GraphQLMiddleware<Root, Context, Args, ResolveType> = (
_ params: ResolverParameters<Root, Context, Args>,
_ next: @escaping () async throws -> ResolveType
) async throws -> ResolveType

/// Build a single resolver with a single base resolver and a handful middlewares
/// - Parameters:
/// - function: The base resolver
/// - middlewares: The middlewares to wrap the resolvers
/// - Returns: A single resolver with middleware applied
public func buildResolver<Root, Context, Args, ResolveType>(
from function: @escaping SyncResolve<Root, Context, Args, ResolveType>,
using middlewares: [GraphQLMiddleware<Root, Context, Args, ResolveType>]
) -> ConcurrentResolve<Root, Context, Args, ResolveType> {
{ root in
{ ctx, args in
let info = ResolverParameters(root: root, context: ctx, args: args)
let result = middlewares
.reversed()
.reduce({ () async throws in try function(root)(ctx, args) }) { next, middleware in
{ () async throws in
try await middleware(info, next)
}
}
return try await result()
}
}
}

/// Build a single resolver with a single base resolver and a handful middlewares
/// - Parameters:
/// - function: The base async resolver
/// - middlewares: The middlewares to wrap the resolvers
/// - Returns: A single resolver with middleware applied
public func buildResolver<Root, Context, Args, ResolveType>(
from function: @escaping ConcurrentResolve<Root, Context, Args, ResolveType>,
using middlewares: [GraphQLMiddleware<Root, Context, Args, ResolveType>]
) -> ConcurrentResolve<Root, Context, Args, ResolveType> {
{ root in
{ ctx, args in
let info = ResolverParameters(root: root, context: ctx, args: args)
let result = middlewares
.reversed()
.reduce({ try await function(root)(ctx, args) }) { next, middleware in
{ () async throws in
try await middleware(info, next)
}
}
return try await result()
}
}
}
6 changes: 3 additions & 3 deletions Sources/Pioneer/GraphQL/GraphQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import GraphQL

/// GraphQL Request according to the spec
public struct GraphQLRequest: Codable {
public struct GraphQLRequest: Codable, @unchecked Sendable {
private enum Key: String, CodingKey, CaseIterable {
case query, operationName, variables, extensions
}
Expand Down Expand Up @@ -105,11 +105,11 @@ public struct GraphQLRequest: Codable {
}

/// Known possible failure in parsing GraphQLRequest
public enum ParsingIssue: Error {
public enum ParsingIssue: Error, @unchecked Sendable {
case missingQuery
case invalidForm
}

/// GraphQL over HTTP spec accept-type
static var acceptType = "application/graphql-response+json"
static var mediaType = "application/graphql-response+json"
}
9 changes: 7 additions & 2 deletions Sources/Pioneer/Http/HTTPGraphQL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import enum NIOHTTP1.HTTPResponseStatus

public extension Pioneer {
/// HTTP-based GraphQL Response
struct HTTPGraphQLResponse {
struct HTTPGraphQLResponse: @unchecked Sendable {
/// GraphQL Result for this response
public var result: GraphQLResult

Expand All @@ -33,7 +33,7 @@ public extension Pioneer {
}

/// HTTP-based GraphQL request
struct HTTPGraphQLRequest {
struct HTTPGraphQLRequest: Sendable {
/// GraphQL Request for this request
public var request: GraphQLRequest

Expand All @@ -60,5 +60,10 @@ public extension Pioneer {
self.headers = headers
self.method = method
}

/// Is request accepting GraphQL media type
public var isAcceptingGraphQLResponse: Bool {
self.headers[.accept].contains(GraphQLRequest.mediaType)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ public extension Request {
return GraphQLRequest(query: query, operationName: operationName, variables: variables)

case .POST:
let accept = self.headers[.accept]
guard !self.headers[.contentType].isEmpty else {
guard !headers[.contentType].isEmpty else {
throw Abort(.badRequest, reason: "Invalid content-type")
}
do {
return try self.content.decode(GraphQLRequest.self)
} catch GraphQLRequest.ParsingIssue.missingQuery {
throw Abort(accept.contains(GraphQLRequest.acceptType) ? .badRequest : .ok,
throw Abort(isAcceptingGraphQLResponse ? .badRequest : .ok,
reason: "Missing query parameter")
} catch GraphQLRequest.ParsingIssue.invalidForm {
throw Abort(accept.contains(GraphQLRequest.acceptType) ? .badRequest : .ok,
throw Abort(isAcceptingGraphQLResponse ? .badRequest : .ok,
reason: "Invalid GraphQL request form")
} catch {
throw Abort(.badRequest, reason: "Unable to parse JSON")
Expand All @@ -48,4 +47,9 @@ public extension Request {
}
}
}

/// Is request accepting GraphQL media type
var isAcceptingGraphQLResponse: Bool {
headers[.accept].contains(GraphQLRequest.mediaType)
}
}
4 changes: 2 additions & 2 deletions Sources/Pioneer/Vapor/Http/Pioneer+Http.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public extension Pioneer {
try res.content.encode(httpRes.result, using: encoder)
res.status = httpRes.status

if req.headers[.accept].contains(GraphQLRequest.acceptType) {
res.headers.replaceOrAdd(name: .contentType, value: "\(GraphQLRequest.acceptType); charset=utf-8, \(GraphQLRequest.acceptType)")
if httpReq.isAcceptingGraphQLResponse {
res.headers.replaceOrAdd(name: .contentType, value: "\(GraphQLRequest.mediaType); charset=utf-8, \(GraphQLRequest.mediaType)")
}
return res
} catch let error as AbortError {
Expand Down

0 comments on commit b466791

Please sign in to comment.