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 {