Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
567 changes: 328 additions & 239 deletions Package.resolved

Large diffs are not rendered by default.

43 changes: 29 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.2
// swift-tools-version:6.0

import PackageDescription

Expand All @@ -10,26 +10,41 @@ let package = Package(
products: [
.library(
name: "GraphQLKit",
targets: ["GraphQLKit"]),
targets: ["GraphQLKit"]
),
],
dependencies: [
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.0.0"),
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.2.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),
],
targets: [
.target(name: "GraphQLKit",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Graphiti", package: "Graphiti"),
.product(name: "Fluent", package: "fluent")
]
.target(
name: "GraphQLKit",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Graphiti", package: "Graphiti"),
.product(name: "Fluent", package: "fluent"),
]
),
.testTarget(name: "GraphQLKitTests",
dependencies: [
.target(name: "GraphQLKit"),
.product(name: "XCTVapor", package: "vapor")
]
.testTarget(
name: "GraphQLKitTests",
dependencies: [
.target(name: "GraphQLKit"),
.product(name: "VaporTesting", package: "vapor"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
]
),
]
)

let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("NonIsolatedNonSendingByDefault"),
]

for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(contentsOf: swiftSettings)
target.swiftSettings = settings
}
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# GraphQLKit
[![Language](https://img.shields.io/badge/Swift-5.2-brightgreen.svg)](http://swift.org)

[![Language](https://img.shields.io/badge/Swift-6.0-brightgreen.svg)](http://swift.org)
[![Vapor Version](https://img.shields.io/badge/Vapor-4-F6CBCA.svg)](http://vapor.codes)
[![build](https://github.com/alexsteinerde/graphql-kit/workflows/build/badge.svg)](https://github.com/alexsteinerde/graphql-kit/actions)


Easy setup of a GraphQL server with Vapor. It uses the GraphQL implementation of [Graphiti](https://github.com/GraphQLSwift/Graphiti).

## Features

- [x] Arguments, operation name and query support
- [x] Normal access to the `Request` object as in normal Vapor request handlers
- [x] Accept JSON in the body of a POST request as the GraphQL query
Expand All @@ -16,6 +17,7 @@ Easy setup of a GraphQL server with Vapor. It uses the GraphQL implementation of
- [ ] Multi-Resolver support

## Installation

```Swift
import PackageDescription

Expand All @@ -31,28 +33,27 @@ let package = Package(
```

## Getting Started

### Define your schema

This package is setup to accept only `Request` objects as the context object for the schema. This gives the opportunity to access all functionality that Vapor provides, for example authentication, service management and database access. To see an example implementation please have a look at the [`vapor-graphql-template`](https://github.com/alexsteinerde/vapor-graphql-template) repository.
This package only provides the needed functions to register an existing GraphQL schema on a Vapor application. To define your schema please refer to the [Graphiti](https://github.com/GraphQLSwift/Graphiti) documentations.
But by including this package some other helper functions are exposed:

#### Async Resolver
An `EventLoopGroup` parameter is no longer required for async resolvers as the `Request` context object already provides access to it's `EventLoopGroup` attribute `eventLoop`.

```Swift
// Instead of adding an unnecessary parameter
func getAllTodos(store: Request, arguments: NoArguments, _: EventLoopGroup) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: store).all()
}
Asynchronously resolve any requests using async/await

// You don't need to provide the eventLoopGroup parameter even when resolving a future.
func getAllTodos(store: Request, arguments: NoArguments) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: store).all()
```Swift
func getAllTodos(store: Request, arguments: NoArguments) async throws -> [Todo] {
try await Todo.query(on: store).all()
}
```

#### Enums
It automatically resolves all cases of an enum if the type conforms to `CaseIterable`.

It automatically resolves all cases of an enum if the type conforms to `CaseIterable`.

```swift
enum TodoState: String, Codable, CaseIterable {
case open
Expand All @@ -63,30 +64,31 @@ enum TodoState: String, Codable, CaseIterable {
Enum(TodoState.self),
```

#### `Parent`, `Children` and `Siblings`
#### `Parent`, `Children` and `Siblings`

Vapor has the functionality to fetch an objects parent, children or siblings automatically with `@Parent`, `@Children` and `@Siblings` types. To integrate this into GraphQL, GraphQLKit provides extensions to the `Field` type that lets you use the parent, children or siblings property as a keypath. Fetching of those related objects is then done automatically.

> :warning: Loading related objects in GraphQL has the [**N+1** problem](https://itnext.io/what-is-the-n-1-problem-in-graphql-dd4921cb3c1a). A solution would be to build a DataLoader package for Swift. But this hasn't been done yet.

```swift
final class User: Model {
...

@Children(for: \.$user)
var todos: [Todo]

...
}

final class Todo: Model {
...

@Parent(key: "user_id")
var user: User

@Siblings(through: TodoTag.self, from: \.$todo, to: \.$tag)
public var tags: [Tag]

...
}
```
Expand All @@ -104,6 +106,7 @@ Type(Todo.self) {
```

### Register the schema on the application

In your `configure.swift` file call the `register(graphQLSchema: Schema<YourResolver, Request>, withResolver: YourResolver)` on your `Application` instance. By default this registers the GET and POST endpoints at `/graphql`. But you can also pass the optional parameter `at:` and override the default value.

```Swift
Expand All @@ -112,7 +115,9 @@ app.register(graphQLSchema: todoSchema, withResolver: TodoAPI())
```

## License

This project is released under the MIT license. See [LICENSE](LICENSE) for details.

## Contribution

You can contribute to this project by submitting a detailed issue or by forking this project and sending a pull request. Contributions of any kind are very welcome :)
2 changes: 1 addition & 1 deletion Sources/GraphQLKit/GraphQLError+Debuggabe.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import GraphQL
import Vapor

extension GraphQLError: AbortError {
extension GraphQLError: @retroactive AbortError {
public var status: HTTPResponseStatus {
return .ok
}
Expand Down
81 changes: 38 additions & 43 deletions Sources/GraphQLKit/Graphiti+Fluent.swift
Original file line number Diff line number Diff line change
@@ -1,98 +1,93 @@
import Fluent
import Graphiti
import Vapor
import Fluent

// Child Relationship
extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {

/// Creates a GraphQL field for a one-to-many relationship for Fluent
public extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {
/// Creates a GraphQL field for a one-to-many relationship for Fluent
/// - Parameters:
/// - name: Filed name
/// - keyPath: KeyPath to the @Children property
public convenience init<ChildType: Model>(
convenience init<ChildType: Model>(
_ name: FieldKey,
with keyPath: KeyPath<ObjectType, ChildrenProperty<ObjectType, ChildType>>
) where FieldType == [TypeReference<ChildType>] {
self.init(name.description, at: { (type) -> (Request, NoArguments, EventLoopGroup) throws -> EventLoopFuture<[ChildType]> in
return { (context: Request, arguments: NoArguments, eventLoop: EventLoopGroup) in
return type[keyPath: keyPath].query(on: context.db).all() // Get the desired property and make the Fluent database query on it.
) where FieldType == [ChildType] {
self.init(name.description) { type in
{ (context: Request, _: NoArguments) async throws in
try await type[keyPath: keyPath].query(on: context.db).all() // Get the desired property and make the Fluent database query on it
}
}, as: [TypeReference<ChildType>].self)
}
}
}

// Parent Relationship
extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {

public extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {
/// Creates a GraphQL field for a one-to-many/one-to-one relationship for Fluent
/// - Parameters:
/// - name: Field name
/// - keyPath: KeyPath to the @Parent property
public convenience init<ParentType: Model>(
convenience init(
_ name: FieldKey,
with keyPath: KeyPath<ObjectType, ParentProperty<ObjectType, ParentType>>
) where FieldType == TypeReference<ParentType> {
self.init(name.description, at: { (type) -> (Request, NoArguments, EventLoopGroup) throws -> EventLoopFuture<ParentType> in
return { (context: Request, arguments: NoArguments, eventLoop: EventLoopGroup) in
return type[keyPath: keyPath].get(on: context.db) // Get the desired property and make the Fluent database query on it.
with keyPath: KeyPath<ObjectType, ParentProperty<ObjectType, FieldType>>
) where FieldType: Model {
self.init(name.description) { type in
{ (context: Request, _: NoArguments) async throws in
return try await type[keyPath: keyPath].get(on: context.db)
}
}, as: TypeReference<ParentType>.self)
}
}
}

// Siblings Relationship
extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {

public extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {
/// Creates a GraphQL field for a many-to-many relationship for Fluent
/// - Parameters:
/// - name: Field name
/// - keyPath: KeyPath to the @Siblings property
public convenience init<ToType: Model, ThroughType: Model>(
convenience init<ToType: Model, ThroughType: Model>(
_ name: FieldKey,
with keyPath: KeyPath<ObjectType, SiblingsProperty<ObjectType, ToType, ThroughType>>
) where FieldType == [TypeReference<ToType>] {
self.init(name.description, at: { (type) -> (Request, NoArguments, EventLoopGroup) throws -> EventLoopFuture<[ToType]> in
return { (context: Request, arguments: NoArguments, eventLoop: EventLoopGroup) in
return type[keyPath: keyPath].query(on: context.db).all() // Get the desired property and make the Fluent database query on it.
) where FieldType == [ToType] {
self.init(name.description) { type in
{ (context: Request, _: NoArguments) async throws in
return try await type[keyPath: keyPath].query(on: context.db).all() // Get the desired property and make the Fluent database query on it
}
}, as: [TypeReference<ToType>].self)
}
}
}

// OptionalParent Relationship
extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {

public extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {
/// Creates a GraphQL field for an optional one-to-many/one-to-one relationship for Fluent
/// - Parameters:
/// - name: Field name
/// - keyPath: KeyPath to the @OptionalParent property
public convenience init<ParentType: Model>(
convenience init<ParentType: Model>(
_ name: FieldKey,
with keyPath: KeyPath<ObjectType, OptionalParentProperty<ObjectType, ParentType>>
) where FieldType == TypeReference<ParentType>? {
self.init(name.description, at: { (type) -> (Request, NoArguments, EventLoopGroup) throws -> EventLoopFuture<Optional<ParentType>> in
return { (context: Request, arguments: NoArguments, eventLoop: EventLoopGroup) throws -> EventLoopFuture<Optional<ParentType>> in
return type[keyPath: keyPath].get(on: context.db) // Get the desired property and make the Fluent database query on it.
) where FieldType == ParentType? {
self.init(name.description) { type in
{ (context: Request, _: NoArguments) async throws -> ParentType? in
return try await type[keyPath: keyPath].get(on: context.db) // Get the desired property and make the Fluent database query on it
}
}, as: TypeReference<ParentType>?.self)
}
}
}

// OptionalChild Relationship
extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {

public extension Graphiti.Field where Arguments == NoArguments, Context == Request, ObjectType: Model {
/// Creates a GraphQL field for an optional one-to-many/one-to-one relationship for Fluent
/// - Parameters:
/// - name: Field name
/// - keyPath: KeyPath to the @OptionalParent property
public convenience init<ParentType: Model>(
convenience init<ParentType: Model>(
_ name: FieldKey,
with keyPath: KeyPath<ObjectType, OptionalChildProperty<ObjectType, ParentType>>
) where FieldType == TypeReference<ParentType>? {
self.init(name.description, at: { (type) -> (Request, NoArguments, EventLoopGroup) throws -> EventLoopFuture<Optional<ParentType>> in
return { (context: Request, arguments: NoArguments, eventLoop: EventLoopGroup) throws -> EventLoopFuture<Optional<ParentType>> in
return type[keyPath: keyPath].get(on: context.db) // Get the desired property and make the Fluent database query on it.
) where FieldType == ParentType? {
self.init(name.description) { type in
{ (context: Request, _: NoArguments) async throws -> ParentType? in
return try await type[keyPath: keyPath].get(on: context.db)
}
}, as: TypeReference<ParentType>?.self)
}
}
}
Loading