Skip to content

Commit

Permalink
Merge pull request #120 from d-exclaimation/format+middleware
Browse files Browse the repository at this point in the history
GraphQL Middleware
  • Loading branch information
d-exclaimation authored Dec 30, 2022
2 parents 6586c80 + b466791 commit 0275b69
Show file tree
Hide file tree
Showing 74 changed files with 983 additions and 695 deletions.
3 changes: 2 additions & 1 deletion Documentation/pages/docs/features/_meta.json
Original file line number Diff line number Diff line change
@@ -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"
}
87 changes: 87 additions & 0 deletions Documentation/pages/docs/features/graphql-middleware.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Callout } from 'nextra-theme-docs'

# GraphQL Middleware

<Callout type="warning" emoji="🚧">
**Work in progress** <br/>
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>

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<Resolver, Context, NoArguments>,
next: @escaping () -> Int
) async throws -> Int {
try await min(next(), 0)
}
```

```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()
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<Root, Args, Returned>() -> GraphQLMiddleware<Root, Context, Args, Returned> {
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<Root, Returned: RESPValueConvertible>(
key: String
) -> GraphQLMiddleware<Root, Context, NoArguments, Returned> {
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()])
}
```
22 changes: 22 additions & 0 deletions Documentation/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Resolver, Void> { ... }

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
Expand Down
17 changes: 10 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"),
]
),
]
)
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Resolver, Void> { ... }

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Pioneer/Extensions/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ public func expression<ReturnType>(_ fn: () throws -> ReturnType) rethrows -> Re
/// - Returns: The returned value of this closure
public func expression<ReturnType>(_ fn: () async throws -> ReturnType) async rethrows -> ReturnType {
try await fn()
}
}
5 changes: 2 additions & 3 deletions Sources/Pioneer/Extensions/Futures/Actor+Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<U>(future: Task<U, Error>, to callback: @Sendable @escaping (Self, Result<U, Error>) async -> Void) {
func pipeToSelf<U>(future: Task<U, Error>, to callback: @Sendable @escaping (Self, Result<U, Error>) async -> Void) {
Task {
do {
let res = try await future.value
Expand Down
5 changes: 2 additions & 3 deletions Sources/Pioneer/Extensions/Int/UInt64+Nanoseconds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
4 changes: 2 additions & 2 deletions Sources/Pioneer/Extensions/Map/Map+Decoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,4 +27,4 @@ public extension Payload {
let data = try JSONEncoder().encode(self)
return try JSONDecoder().decode(dataType, from: data)
}
}
}
2 changes: 1 addition & 1 deletion Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public extension Pioneer {
timeout: timeout
)
}

/// - Parameters:
/// - schema: GraphQL schema used to execute operations
/// - resolver: Resolver used by the GraphQL schema
Expand Down
2 changes: 1 addition & 1 deletion Sources/Pioneer/Extensions/Results/Data+Json.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import class Foundation.JSONDecoder

extension Data {
/// Parse data into any Decodable type if possible, otherwise return nil
func to<T: Decodable>(_ type: T.Type) -> T? {
func to<T: Decodable>(_: T.Type) -> T? {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(T.self, from: self)
Expand Down
27 changes: 13 additions & 14 deletions Sources/Pioneer/GraphQL/BuiltinTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) {
Expand Down Expand Up @@ -56,15 +56,15 @@ 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..<length).compactMap { _ in letters.randomElement() }
let random = (0 ..< length).compactMap { _ in letters.randomElement() }
return .init(.init(random))
}

/// Length of ID
public var count: Int {
id.count
}

/// String value of this ID type
public var string: String {
id
Expand All @@ -78,14 +78,14 @@ public struct ID : Codable, ExpressibleByStringLiteral, CustomStringConvertible,
/// Get a UUID from this ID scalar
/// - Returns: The UUID object if possible otherwise throw an error
public func toUUID() throws -> 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

Expand All @@ -103,27 +103,26 @@ 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)
}
}

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)
}
}
}
Loading

0 comments on commit 0275b69

Please sign in to comment.