diff --git a/Documentation/pages/docs/features/_meta.json b/Documentation/pages/docs/features/_meta.json
index f5b3dff..39102a9 100644
--- a/Documentation/pages/docs/features/_meta.json
+++ b/Documentation/pages/docs/features/_meta.json
@@ -1,5 +1,6 @@
{
"graphql-over-http": "GraphQL over HTTP",
"graphql-over-websocket": "GraphQL over WebSocket",
- "graphql-ide": "GraphQL IDE"
+ "graphql-ide": "GraphQL IDE",
+ "graphql-middleware": "GraphQL Middleware"
}
\ No newline at end of file
diff --git a/Documentation/pages/docs/features/graphql-middleware.mdx b/Documentation/pages/docs/features/graphql-middleware.mdx
new file mode 100644
index 0000000..4faaadd
--- /dev/null
+++ b/Documentation/pages/docs/features/graphql-middleware.mdx
@@ -0,0 +1,87 @@
+import { Callout } from 'nextra-theme-docs'
+
+# GraphQL Middleware
+
+
+**Work in progress**
+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.
+
+
+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 - _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,
+ next: @escaping () -> Int
+) async throws -> Int {
+ try await min(next(), 0)
+}
+```
+
+```swift {3,5-6} showLineNumbers copy filename="Reusable generic middleware"
+public func Trace() -> GraphQLMiddleware {
+ return { params, next in
+ let before = Date()
+ let res = try await next()
+ let after = Date()
+ params.ctx.logger.info("Operation takes \(after.timeIntervalSince(before))s")
+ return res
+ }
+}
+```
+
+### Intercepting the resolver
+
+Middleware also has the ability to intercept the result of a resolver's execution.
+
+As an example would be an auth guard:
+
+```swift {3-5} showLineNumbers copy
+public func Auth() -> GraphQLMiddleware {
+ return { params, next in
+ guard case .some = params.ctx.user else {
+ throw GraphQLError(message: "Not authenticated")
+ }
+ return try await next()
+ }
+}
+```
+
+Another one would be a cache interceptor:
+
+```swift {5-7} showLineNumbers copy
+public func Cached(
+ key: String
+) -> GraphQLMiddleware {
+ return { params, next in
+ guard let cache = try? await redis.get(key, as: Returned.self).get() else {
+ return cache
+ }
+ return try await next()
+ }
+}
+```
+
+## Attaching Middleware
+
+Attaching middlewares can be done through the `use` parameter from `Field`. The order of the middleware is also important.
+
+```swift {3,8} showLineNumbers copy
+Query {
+ // Trace -> Cached -> Resolver
+ Field("books", at: Resolver.books, use: [Trace(), Cached(key: "query:books")])
+}
+
+Mutation {
+ // Auth -> Trace -> Resolver
+ Field("createBook", at: Resolver.createBook, use: [Auth(), Trace()])
+}
+```
\ No newline at end of file
diff --git a/Documentation/pages/index.mdx b/Documentation/pages/index.mdx
index d9cbd59..cb367de 100644
--- a/Documentation/pages/index.mdx
+++ b/Documentation/pages/index.mdx
@@ -16,9 +16,31 @@ import Headers from '../components/header'
.package(url: "https://github.com/d-exclaimation/pioneer", from: "1.0.0")
```
+### Quick start
+
+```swift showLineNumbers copy
+import Graphiti
+import Pioneer
+
+struct Resolver { ... }
+
+let schema = try Schema { ... }
+
+let server = Pioneer(
+ schema: schema,
+ resolver: .init()
+)
+
+try server.standaloneServer(
+ port: 4000,
+ host: "127.0.0.1"
+)
+```
+
- [Documentation](https://pioneer.dexclaimation.com/docs)
- [Getting started](https://pioneer.dexclaimation.com/docs/getting-started)
- [API References](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation)
+- [GraphQL over HTTP audit report](https://github.com/graphql/graphql-http#servers)
- [Example](https://github.com/d-exclaimation/pioneer-example)
### Attribution
diff --git a/Package.swift b/Package.swift
index 1e4ba26..4897f3e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -6,20 +6,21 @@ import PackageDescription
let package = Package(
name: "Pioneer",
platforms: [
- .macOS(.v12)
+ .macOS(.v12),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Pioneer",
- targets: ["Pioneer"]),
+ targets: ["Pioneer"]
+ ),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "2.4.0"),
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.2.1"),
- .package(url: "https://github.com/vapor/vapor.git", from: "4.67.1")
+ .package(url: "https://github.com/vapor/vapor.git", from: "4.67.1"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -28,13 +29,15 @@ let package = Package(
name: "Pioneer",
dependencies: [
"GraphQL", "Graphiti",
- .product(name: "Vapor", package: "vapor")
- ]),
+ .product(name: "Vapor", package: "vapor"),
+ ]
+ ),
.testTarget(
name: "PioneerTests",
dependencies: [
"Pioneer",
- .product(name: "XCTVapor", package: "vapor")
- ]),
+ .product(name: "XCTVapor", package: "vapor"),
+ ]
+ ),
]
)
diff --git a/README.md b/README.md
index a072212..ed3eca5 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,27 @@ Pioneer is an open-source, [spec-compliant](https://github.com/graphql/graphql-h
.package(url: "https://github.com/d-exclaimation/pioneer", from: "1.0.0")
```
+### Quick start
+
+```swift
+import Graphiti
+import Pioneer
+
+struct Resolver { ... }
+
+let schema = try Schema { ... }
+
+let server = Pioneer(
+ schema: schema,
+ resolver: .init()
+)
+
+try server.standaloneServer(
+ port: 4000,
+ host: "127.0.0.1"
+)
+```
+
## Usage/Examples
- [Documentation](https://pioneer.dexclaimation.com/docs)
diff --git a/Sources/Pioneer/Extensions/Collections/OrderedDictionary+Dictionary.swift b/Sources/Pioneer/Extensions/Collections/OrderedDictionary+Dictionary.swift
index 0c36674..f317133 100644
--- a/Sources/Pioneer/Extensions/Collections/OrderedDictionary+Dictionary.swift
+++ b/Sources/Pioneer/Extensions/Collections/OrderedDictionary+Dictionary.swift
@@ -7,11 +7,11 @@
import struct OrderedCollections.OrderedDictionary
-extension OrderedDictionary {
+public extension OrderedDictionary {
/// Turning OrderedDictionary into a regular one as both aren't API compatible.
- public func unordered() -> [Key: Value] {
+ func unordered() -> [Key: Value] {
var res = [Key: Value]()
- forEach { (key, val) in
+ forEach { key, val in
res[key] = val
}
return res
diff --git a/Sources/Pioneer/Extensions/Expression.swift b/Sources/Pioneer/Extensions/Expression.swift
index 1e20b49..29287d3 100644
--- a/Sources/Pioneer/Extensions/Expression.swift
+++ b/Sources/Pioneer/Extensions/Expression.swift
@@ -15,4 +15,4 @@ public func expression(_ fn: () throws -> ReturnType) rethrows -> Re
/// - Returns: The returned value of this closure
public func expression(_ fn: () async throws -> ReturnType) async rethrows -> ReturnType {
try await fn()
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/Extensions/Futures/Actor+Task.swift b/Sources/Pioneer/Extensions/Futures/Actor+Task.swift
index 1c72e6e..b79602f 100644
--- a/Sources/Pioneer/Extensions/Futures/Actor+Task.swift
+++ b/Sources/Pioneer/Extensions/Futures/Actor+Task.swift
@@ -5,14 +5,13 @@
// Created by d-exclaimation on 11:41 PM.
//
-
-extension Actor {
+public extension Actor {
/// Method for handling NIO EventLoopFuture with an Actor using the pipe pattern
///
/// - Parameters:
/// - future: EventLoopFuture value being awaited
/// - to: Transforming callback to for the result from the Future.
- public func pipeToSelf(future: Task, to callback: @Sendable @escaping (Self, Result) async -> Void) {
+ func pipeToSelf(future: Task, to callback: @Sendable @escaping (Self, Result) async -> Void) {
Task {
do {
let res = try await future.value
diff --git a/Sources/Pioneer/Extensions/Int/UInt64+Nanoseconds.swift b/Sources/Pioneer/Extensions/Int/UInt64+Nanoseconds.swift
index 8ff73ad..46487f3 100644
--- a/Sources/Pioneer/Extensions/Int/UInt64+Nanoseconds.swift
+++ b/Sources/Pioneer/Extensions/Int/UInt64+Nanoseconds.swift
@@ -20,11 +20,10 @@ public extension Optional where WrappedType == UInt64 {
s * 1_000_000
}
-
/// Convert the given value in microseconds into nanoseconds
/// - Parameter s: The value in microseconds
/// - Returns: The nanoseconds result
static func microseconds(_ s: UInt64) -> UInt64 {
- s * 1_000
+ s * 1000
}
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/Extensions/Map/Map+Decoder.swift b/Sources/Pioneer/Extensions/Map/Map+Decoder.swift
index 0c152c9..e687dbc 100644
--- a/Sources/Pioneer/Extensions/Map/Map+Decoder.swift
+++ b/Sources/Pioneer/Extensions/Map/Map+Decoder.swift
@@ -5,8 +5,8 @@
// Created by d-exclaimation on 22:20.
//
-import class Foundation.JSONEncoder
import class Foundation.JSONDecoder
+import class Foundation.JSONEncoder
import enum GraphQL.Map
public extension Map {
@@ -27,4 +27,4 @@ public extension Payload {
let data = try JSONEncoder().encode(self)
return try JSONDecoder().decode(dataType, from: data)
}
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/Extensions/Pioneer+Graphiti.swift b/Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
index 06d6c01..f375f1b 100644
--- a/Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
+++ b/Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
@@ -42,7 +42,7 @@ public extension Pioneer {
timeout: timeout
)
}
-
+
/// - Parameters:
/// - schema: GraphQL schema used to execute operations
/// - resolver: Resolver used by the GraphQL schema
diff --git a/Sources/Pioneer/Extensions/Results/Data+Json.swift b/Sources/Pioneer/Extensions/Results/Data+Json.swift
index a0463c7..378962b 100644
--- a/Sources/Pioneer/Extensions/Results/Data+Json.swift
+++ b/Sources/Pioneer/Extensions/Results/Data+Json.swift
@@ -10,7 +10,7 @@ import class Foundation.JSONDecoder
extension Data {
/// Parse data into any Decodable type if possible, otherwise return nil
- func to(_ type: T.Type) -> T? {
+ func to(_: T.Type) -> T? {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(T.self, from: self)
diff --git a/Sources/Pioneer/GraphQL/BuiltinTypes.swift b/Sources/Pioneer/GraphQL/BuiltinTypes.swift
index b1e776d..de0065e 100644
--- a/Sources/Pioneer/GraphQL/BuiltinTypes.swift
+++ b/Sources/Pioneer/GraphQL/BuiltinTypes.swift
@@ -6,15 +6,15 @@
//
import Foundation
-import GraphQL
import Graphiti
+import GraphQL
public typealias NoArgs = NoArguments
/// The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache.
///
/// The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be humanâreadable.
-public struct ID : Codable, ExpressibleByStringLiteral, CustomStringConvertible, Hashable, Sendable {
+public struct ID: Codable, ExpressibleByStringLiteral, CustomStringConvertible, Hashable, Sendable {
/// Inner string properties
private var id: String
@@ -23,7 +23,7 @@ public struct ID : Codable, ExpressibleByStringLiteral, CustomStringConvertible,
}
public init(uuid: UUID) {
- self.id = uuid.uuidString
+ id = uuid.uuidString
}
public init(stringLiteral value: String) {
@@ -56,7 +56,7 @@ public struct ID : Codable, ExpressibleByStringLiteral, CustomStringConvertible,
/// Create a new ID from random letter
public static func random(length: Int = 10) -> Self {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- let random = (0.. UUID {
- guard let uuid = self.uuid else {
+ guard let uuid = uuid else {
throw ConversionError(id: id, reason: "Cannot convert this ID that is not in UUID string format to UUID")
}
return uuid
}
/// 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
@@ -103,13 +103,12 @@ public struct ID : Codable, ExpressibleByStringLiteral, CustomStringConvertible,
}
}
-
public extension String {
/// ID from this string
var id: ID {
.init(self)
}
-
+
/// ID from this string
func toID() -> ID {
.init(self)
@@ -117,13 +116,13 @@ public extension String {
}
public extension UUID {
- /// ID from this string
+ /// ID from this string
var id: ID {
- .init(self.uuidString)
+ .init(uuidString)
}
-
+
/// ID from this string
func toID() -> ID {
- .init(self.uuidString)
+ .init(uuidString)
}
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/GraphQL/Extensions/Field+AsyncAwait.swift b/Sources/Pioneer/GraphQL/Extensions/Field+AsyncAwait.swift
index ee372a4..0752b2a 100644
--- a/Sources/Pioneer/GraphQL/Extensions/Field+AsyncAwait.swift
+++ b/Sources/Pioneer/GraphQL/Extensions/Field+AsyncAwait.swift
@@ -6,8 +6,8 @@
//
import Graphiti
-import protocol NIO.EventLoopGroup
import GraphQL
+import protocol NIO.EventLoopGroup
/// Async-await non-throwing GraphQL resolver function
public typealias AsyncAwaitResolveWithEventLoop = (ObjectType) -> (Context, Arguments, EventLoopGroup) async -> FieldType
@@ -15,7 +15,7 @@ public typealias AsyncAwaitResolveWithEventLoop = (ObjectType) -> (Context, Arguments, EventLoopGroup) async throws -> FieldType
-public extension Graphiti.Field where FieldType : Encodable {
+public extension Graphiti.Field where FieldType: Encodable {
// -- (context, args, eventLoop) async -> result
convenience init(
@@ -44,7 +44,7 @@ public extension Graphiti.Field where FieldType : Encodable {
}
}
}
- self.init(name, at: resolve, {})
+ self.init(name, at: resolve) {}
}
// -- (context, args, eventLoop) async throws -> result
@@ -75,12 +75,10 @@ public extension Graphiti.Field where FieldType : Encodable {
}
}
}
- self.init(name, at: resolve, {})
+ self.init(name, at: resolve) {}
}
-
}
-
public extension Graphiti.Field {
// -- (context, args, eventLoop) async -> result
@@ -112,7 +110,7 @@ public extension Graphiti.Field {
}
}
}
- self.init(name, at: resolve, as: `as`, {})
+ self.init(name, at: resolve, as: `as`) {}
}
// -- (context, args, eventLoop) async throws -> result
@@ -132,7 +130,7 @@ public extension Graphiti.Field {
}
self.init(name, at: resolve, as: `as`, argument)
}
-
+
convenience init(
_ name: String,
at function: @escaping AsyncAwaitThrowingResolveWithEventLoop,
@@ -145,7 +143,6 @@ public extension Graphiti.Field {
}
}
}
- self.init(name, at: resolve, as: `as`, {})
+ self.init(name, at: resolve, as: `as`) {}
}
-
}
diff --git a/Sources/Pioneer/GraphQL/Extensions/Field+Middleware.swift b/Sources/Pioneer/GraphQL/Extensions/Field+Middleware.swift
new file mode 100644
index 0000000..fdf86ad
--- /dev/null
+++ b/Sources/Pioneer/GraphQL/Extensions/Field+Middleware.swift
@@ -0,0 +1,68 @@
+//
+// Field+Middleware.swift
+// pioneer
+//
+// Created by d-exclaimation on 00:13.
+//
+
+import class Graphiti.ArgumentComponent
+import struct Graphiti.ArgumentComponentBuilder
+import typealias Graphiti.ConcurrentResolve
+import class Graphiti.Field
+import typealias Graphiti.SyncResolve
+
+public extension Field where FieldType: Encodable {
+ convenience init(
+ _ name: String,
+ at function: @escaping SyncResolve,
+ use middlewares: [GraphQLMiddleware],
+ @ArgumentComponentBuilder _ argument: () -> ArgumentComponent
+ ) {
+ self.init(
+ name: name,
+ arguments: [argument()],
+ concurrentResolve: buildResolver(from: function, using: middlewares)
+ )
+ }
+
+ convenience init(
+ _ name: String,
+ at function: @escaping SyncResolve,
+ use middlewares: [GraphQLMiddleware],
+ @ArgumentComponentBuilder _ arguments: ()
+ -> [ArgumentComponent] = { [] }
+ ) {
+ self.init(
+ name: name,
+ arguments: arguments(),
+ concurrentResolve: buildResolver(from: function, using: middlewares)
+ )
+ }
+
+ convenience init(
+ _ name: String,
+ at function: @escaping ConcurrentResolve,
+ use middlewares: [GraphQLMiddleware],
+ @ArgumentComponentBuilder _ argument: () -> ArgumentComponent
+ ) {
+ self.init(
+ name: name,
+ arguments: [argument()],
+ concurrentResolve: buildResolver(from: function, using: middlewares)
+ )
+ }
+
+ convenience init(
+ _ name: String,
+ at function: @escaping ConcurrentResolve,
+ use middlewares: [GraphQLMiddleware],
+ @ArgumentComponentBuilder _ arguments: ()
+ -> [ArgumentComponent] = { [] }
+ ) {
+ self.init(
+ name: name,
+ arguments: arguments(),
+ concurrentResolve: buildResolver(from: function, using: middlewares)
+ )
+ }
+}
diff --git a/Sources/Pioneer/GraphQL/Extensions/GraphQL+Execution.swift b/Sources/Pioneer/GraphQL/Extensions/GraphQL+Execution.swift
index db2f07f..ba8229e 100644
--- a/Sources/Pioneer/GraphQL/Extensions/GraphQL+Execution.swift
+++ b/Sources/Pioneer/GraphQL/Extensions/GraphQL+Execution.swift
@@ -5,13 +5,11 @@
// Created by d-exclaimation on 9:46 PM.
//
-
import GraphQL
import protocol NIO.EventLoopGroup
-
/// Execute request-response based GraphQL Operation
-///
+///
/// - Parameters:
/// - schema: GraphQL Schema used to execute request
/// - request: Query request string
@@ -42,7 +40,6 @@ public func executeGraphQL(
).get()
}
-
/// Execute streaming based GraphQL Operation
///
/// - Parameters:
diff --git a/Sources/Pioneer/GraphQL/Extensions/GraphQLError+Error.swift b/Sources/Pioneer/GraphQL/Extensions/GraphQLError+Error.swift
index 873b400..cc693f8 100644
--- a/Sources/Pioneer/GraphQL/Extensions/GraphQLError+Error.swift
+++ b/Sources/Pioneer/GraphQL/Extensions/GraphQLError+Error.swift
@@ -7,9 +7,9 @@
import struct GraphQL.GraphQLError
-extension Error {
+public extension Error {
/// Get the GraphQLError version of this error
- public var graphql: GraphQLError {
+ var graphql: GraphQLError {
self as? GraphQLError ?? .init(self)
}
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/GraphQL/GraphQLMessage.swift b/Sources/Pioneer/GraphQL/GraphQLMessage.swift
index 2ca8d4e..fe7bef0 100644
--- a/Sources/Pioneer/GraphQL/GraphQLMessage.swift
+++ b/Sources/Pioneer/GraphQL/GraphQLMessage.swift
@@ -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
@@ -27,11 +27,11 @@ public struct GraphQLMessage: Codable {
static func from(type: String, id: String? = nil, _ gql: GraphQL.GraphQLResult) -> GraphQLMessage {
let errors = parseError(gql.errors)
switch (gql.data, errors) {
- case (.some(let data), .some(let errors)):
+ case let (.some(data), .some(errors)):
return .init(id: id, type: type, payload: ["data": data, "errors": errors])
- case (.some(let data), .none):
+ case let (.some(data), .none):
return .init(id: id, type: type, payload: ["data": data])
- case (.none, .some(let errors)):
+ case let (.none, .some(errors)):
return .init(id: id, type: type, payload: ["errors": errors])
case (.none, .none):
return .init(id: id, type: type)
@@ -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?
@@ -76,6 +76,6 @@ extension Encodable {
/// Any encodable into JSON String otherwise null is returned
var jsonString: String {
- json.flatMap { String(data: $0, encoding: .utf8)} ?? "null"
+ json.flatMap { String(data: $0, encoding: .utf8) } ?? "null"
}
}
diff --git a/Sources/Pioneer/GraphQL/GraphQLMiddleware.swift b/Sources/Pioneer/GraphQL/GraphQLMiddleware.swift
new file mode 100644
index 0000000..57d63ff
--- /dev/null
+++ b/Sources/Pioneer/GraphQL/GraphQLMiddleware.swift
@@ -0,0 +1,78 @@
+//
+// GraphQLMiddleware.swift
+// pioneer
+//
+// 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 {
+ /// The root element
+ public var root: Root
+ /// The context given for this operation
+ public var context: Context
+ /// The resolver arguments
+ public var args: Args
+}
+
+/// Field resolver middleware specification
+///
+/// - Parameters:
+/// - params: The resolver parameters
+/// - next: The next function to be called
+/// - Returns: The return type for the field resolver
+public typealias GraphQLMiddleware = (
+ _ params: ResolverParameters,
+ _ 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(
+ from function: @escaping SyncResolve,
+ using middlewares: [GraphQLMiddleware]
+) -> ConcurrentResolve {
+ { 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(
+ from function: @escaping ConcurrentResolve,
+ using middlewares: [GraphQLMiddleware]
+) -> ConcurrentResolve {
+ { 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()
+ }
+ }
+}
diff --git a/Sources/Pioneer/GraphQL/GraphQLRequest.swift b/Sources/Pioneer/GraphQL/GraphQLRequest.swift
index d6eff5b..58cc2aa 100644
--- a/Sources/Pioneer/GraphQL/GraphQLRequest.swift
+++ b/Sources/Pioneer/GraphQL/GraphQLRequest.swift
@@ -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
}
@@ -42,27 +42,26 @@ public struct GraphQLRequest: Codable {
let variables = try container.decodeIfPresent([String: Map]?.self, forKey: .variables)
let extensions = try container.decodeIfPresent([String: Map]?.self, forKey: .extensions)
self.init(
- query: query,
- operationName: operationName ?? nil,
- variables: variables ?? nil,
+ query: query,
+ operationName: operationName ?? nil,
+ variables: variables ?? nil,
extensions: extensions ?? nil
)
} catch {
throw ParsingIssue.invalidForm
}
}
-
public init(
- query: String,
- operationName: String? = nil,
+ query: String,
+ operationName: String? = nil,
variables: [String: Map]? = nil,
- extensions: [String: Map]? = nil
+ extensions _: [String: Map]? = nil
) {
self.query = query
self.operationName = operationName
self.variables = variables
- self.ast = try? parse(source: .init(body: query))
+ ast = try? parse(source: .init(body: query))
}
public func encode(to encoder: Encoder) throws {
@@ -71,7 +70,7 @@ public struct GraphQLRequest: Codable {
try container.encodeIfPresent(operationName, forKey: .operationName)
try container.encodeIfPresent(variables, forKey: .variables)
try container.encodeIfPresent(extensions, forKey: .extensions)
- }
+ }
/// Getting parsed operationType
public var operationType: OperationType? {
@@ -80,11 +79,11 @@ public struct GraphQLRequest: Codable {
.compactMap { def -> OperationDefinition? in
def as? OperationDefinition
}
-
+
guard let operationName = operationName else {
return operations.first?.operation
}
-
+
return operations
.first {
guard let name = $0.name?.value else { return false }
@@ -106,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"
}
diff --git a/Sources/Pioneer/Http/HTTPGraphQL.swift b/Sources/Pioneer/Http/HTTPGraphQL.swift
index 826c9fe..f2c53a1 100644
--- a/Sources/Pioneer/Http/HTTPGraphQL.swift
+++ b/Sources/Pioneer/Http/HTTPGraphQL.swift
@@ -5,16 +5,16 @@
// Created by d-exclaimation on 22:16.
//
-import enum NIOHTTP1.HTTPResponseStatus
-import enum NIOHTTP1.HTTPMethod
+import struct GraphQL.GraphQLError
+import struct GraphQL.GraphQLResult
import enum GraphQL.Map
import struct NIOHTTP1.HTTPHeaders
-import struct GraphQL.GraphQLResult
-import struct GraphQL.GraphQLError
+import enum NIOHTTP1.HTTPMethod
+import enum NIOHTTP1.HTTPResponseStatus
-extension Pioneer {
+public extension Pioneer {
/// HTTP-based GraphQL Response
- public struct HTTPGraphQLResponse {
+ struct HTTPGraphQLResponse: @unchecked Sendable {
/// GraphQL Result for this response
public var result: GraphQLResult
@@ -33,11 +33,11 @@ extension Pioneer {
}
/// HTTP-based GraphQL request
- public struct HTTPGraphQLRequest {
+ struct HTTPGraphQLRequest: Sendable {
/// GraphQL Request for this request
public var request: GraphQLRequest
- /// HTTP headers given in this request
+ /// HTTP headers given in this request
public var headers: HTTPHeaders
/// HTTP method for this request
@@ -53,12 +53,17 @@ extension Pioneer {
query: String,
operationName: String? = nil,
variables: [String: Map]? = nil,
- headers: HTTPHeaders,
- method: HTTPMethod
+ headers: HTTPHeaders,
+ method: HTTPMethod
) {
self.request = .init(query: query, operationName: operationName, variables: variables)
self.headers = headers
self.method = method
}
+
+ /// Is request accepting GraphQL media type
+ public var isAcceptingGraphQLResponse: Bool {
+ self.headers[.accept].contains(GraphQLRequest.mediaType)
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Sources/Pioneer/Http/HttpStrategy.swift b/Sources/Pioneer/Http/HttpStrategy.swift
index d8b7b92..2aecdad 100644
--- a/Sources/Pioneer/Http/HttpStrategy.swift
+++ b/Sources/Pioneer/Http/HttpStrategy.swift
@@ -5,12 +5,12 @@
// Created by d-exclaimation on 4:30 PM.
//
-import enum NIOHTTP1.HTTPMethod
import enum GraphQL.OperationType
+import enum NIOHTTP1.HTTPMethod
-extension Pioneer {
+public extension Pioneer {
/// HTTP Operation and routing strategy for GraphQL
- public enum HTTPStrategy {
+ enum HTTPStrategy {
/// Only allow `POST` GraphQL Request, most common choice
case onlyPost
/// Only allow `GET` GraphQL Request, not recommended for most
@@ -25,7 +25,7 @@ extension Pioneer {
case csrfPrevention
/// Allow all operation through `GET` and `POST`.
case both
-
+
/// Get the allowed operation for aa type of HTTPMethod
/// - Parameter method: The HTTP Method this operation is executed
/// - Returns: A list of allowed GraphQL Operation Type
diff --git a/Sources/Pioneer/Http/IDE.swift b/Sources/Pioneer/Http/IDE.swift
index 1ace49b..cd7b881 100644
--- a/Sources/Pioneer/Http/IDE.swift
+++ b/Sources/Pioneer/Http/IDE.swift
@@ -5,46 +5,45 @@
// Created by d-exclaimation on 14:17.
//
-extension Pioneer {
+public extension Pioneer {
/// GraphQL Hosted IDE
- public enum IDE: Equatable {
+ enum IDE: Equatable {
@available(*, deprecated, message: "Use `GraphiQL or Apollo Sandbox instead`")
case playground
-
+
/// GraphiQL Browser IDE
case graphiql
-
+
/// Embedded Apollo Sandbox
case sandbox
-
+
/// Redirect to a cloud based IDE
case redirect(to: Cloud)
-
+
/// Disabled any IDEs
case disable
-
+
public enum Cloud {
/// Cloud version of Apollo Sandbox
case apolloSandbox
-
+
/// Cloud version of Banana Cake Pop
case bananaCakePop
/// URL for Cloud-based IDE
public var url: String {
- switch (self) {
- case .apolloSandbox:
- return "https://studio.apollographql.com/sandbox/explorer"
- case .bananaCakePop:
- return "https://eat.bananacakepop.com"
+ switch self {
+ case .apolloSandbox:
+ return "https://studio.apollographql.com/sandbox/explorer"
+ case .bananaCakePop:
+ return "https://eat.bananacakepop.com"
}
}
}
-
}
-
+
/// GraphQL Playground HTML
- public var playgroundHtml: String {
+ var playgroundHtml: String {
let graphqlPlayground = """
@@ -115,7 +114,7 @@ extension Pioneer {
}
/// GraphiQL HTML
- public var graphiqlHtml: String {
+ var graphiqlHtml: String {
let fetcher: String = expression {
switch websocketProtocol {
case .subscriptionsTransportWs:
@@ -124,9 +123,9 @@ extension Pioneer {