Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PubSub and Broadcast #45

Merged
merged 17 commits into from
Jun 22, 2022
Merged
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
26 changes: 26 additions & 0 deletions Documentation/guides/features/async-event-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Termination callback can be implicitly inferred for these types of `AsyncSequenc

- `AsyncStream`
- `AsyncPubSub` (_due to `AsyncStream`_)
- `PubSub` (_due to `AsyncStream`_)

+++ AsyncPubSub

Expand Down Expand Up @@ -125,3 +126,28 @@ let eventStream: EventStream<Quake> = stream
+++

!!!

## PubSub

In cases where you need to have a Pub/Sub system for event streams, Pioneer provide a protocol to define an implemention for [AsyncPubSub](/references/async-pubsub) named [PubSub](#pubsub) which allow you to use AsyncPubSub to start with and later move on to PubSub implementation backed by popular event-publishing systems that worked better for distributed systems.

```swift Context.swift
struct Context {
var pubsub: PubSub
}

```

```swift main.swift
let pubsub: PubSub = app.environment.isRelease ? CustomKafkaPubSub(...) : AsyncPubSub()

let pioneer = Pioneer(
...,
contextBuilder: { req, res in
Context(req, res, pubsub)
},
...
)
```

[!ref More on PubSub]()
150 changes: 145 additions & 5 deletions Documentation/guides/features/fluent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
icon: database
order: 60
order: 50
---

# Fluent
Expand Down Expand Up @@ -72,6 +72,8 @@ func schema() throws -> Schema<Resolver, Context> {

## Fluent Relationship

### Relationship Resolver

Say we have a new struct `Item` that have a many to one relationship to `User`. You can easily describe this into the GraphQL schema with using Swift's extension.

```swift Item.swift
Expand Down Expand Up @@ -102,6 +104,8 @@ final class Item: Model, Content {

Using extensions, we can describe a custom resolver function to fetch the `User` for the `Item`.

##### Resolver on Item

```swift Item+GraphQL.swift
import Foundation
import Fluent
Expand All @@ -111,14 +115,17 @@ import Graphiti

extension Item {
func owner(ctx: Context, _: NoArguments) async throws -> User? {
return try await User.query(on: ctx.req.db).filter(\.$id == $user.id).first()
return try await User.find($user.id, on: ctx.req.db)
}
}
```

_This example is very simple. However in a real application, you might want to use [`DataLoader`](https://github.com/GraphQLSwift/DataLoader)_
!!!warning N+1 problem

[!ref More on Relationship](/guides/getting-started/resolver.md/#relationship)
In a real producation application, this example resolver is flawed with the [N+1 problem](#n1-problem).

[!ref More on N+1 problem](#n1-problem)
!!!

And update the schema accordingly.

Expand Down Expand Up @@ -148,5 +155,138 @@ func schema() throws -> Schema<Resolver, Context> {

This approach is actually not a specific to Pioneer. You can use the same or similar solutions if you are using Vapor, Fluent, and Graphiti, albeit without some features provided by Pioneer (i.e. async await resolver, and custom ID struct).

### N+1 Problem

Imagine your graph has query that lists items

```graphql
query {
items {
name
owner {
id
name
}
}
}
```

with the `items` resolver looked like

```swift Resolver.swift
struct Resolver {
func items(ctx: Context, _: NoArguments) async throws -> [Item] {
try await Item.query(on: ctx.req.d).all()
}
}
```

and the `Item` has relationship resolver looked like [`Item.owner`](#resolver-on-item).

The graph will executed that `Resolver.items` function which will make a request to the database to get all items.

Furthermore for each item, the graph will also execute the `Item.owner` function which make another request to the databse to get the user with the given id. Resulting in the following SQL statements:

```SQL N+1 queries
SELECT * FROM items
SELECT * FROM users WHERE id = ?
SELECT * FROM users WHERE id = ?
SELECT * FROM users WHERE id = ?
SELECT * FROM users WHERE id = ?
SELECT * FROM users WHERE id = ?
...
```

What's worse is that certain items can be owned by the same user so these statements will likely query for the same users multiple times.

This is what's called the N+1 problem which you want to avoid. The solution? [DataLoader](#dataloader).

### DataLoader

The GraphQL Foundation provided a specification for solution to the [N+1 problem](#n1-problem) called `dataloader`. Essentially, dataloaders combine the fetching of process across all resolvers for a given GraphQL request into a single query.

!!!success DataLoader with async-await
Since `v0.5.2`, Pioneer already provide extensions to use DataLoader with async await
!!!

The package [Dataloader](https://github.com/GraphQLSwift/DataLoader) implement that solution for [GraphQLSwift/GraphQL](https://github.com/GraphQLSwift/DataLoader).

```swift Adding DataLoader
.package(url: "https://github.com/GraphQLSwift/DataLoader", from: "...")
```

After that, we can create a function to build a new dataloader for each `Request`, and update the relationship resolver to use the loader

```swift Loader and Context
struct Context {
...
// Loader computed on each Context or each request
var userLoader: DataLoader<UUID, User>
}

extension User {
func makeLoader(req: Request) -> DataLoader<UUID, User> {
.init(on: req.eventLoop) { keys async in
let users = try? await User.query(on: req.db).filter(\.$id ~~ keys).all()
return keys.map { key in
guard let user = res?.first(where: { $0.id == key }) else {
return .error(GraphQLError(
message: "No user with corresponding key: \(key)"
))
}
return .success(user)
}
}
}
}
```

!!!success Loading Many
In cases where you have an arrays of ids of users and need to fetch those users in a relationship resolver, [Dataloader](https://github.com/GraphQLSwift/DataLoader) have a method called `loadMany` which takes multiple keys and return them all.

In other cases where you have the user id but need to fetch all items with that user id, you can just have the loader be `DataLoader<UUID, [Item]>` where the `UUID` is the user id and now `load` should return an array of `Item`.
!!!

```swift Item+GraphQL.swift
extension Item {
func owner(ctx: Context, _: NoArguments, ev: EventLoopGroup) async throws -> User? {
guard let uid = $user.id else {
return nil
}
return try await ctx.userLoader.load(key: uid, on: ev.next()).get()
}
}
```

Now instead of having n+1 queries to the database by using the dataloader, the only SQL queries sent to the database are:

```SQL
SELECT * FROM items
SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?, ...)
```

which is significantly better.

#### EagerLoader

Fluent provides a way to eagerly load relationship which will solve the N+1 problem by joining the SQL statement.

However, it forces you fetch the relationship **regardless** whether it is requested in the GraphQL operation which can be considered **overfetching**.

```swift Resolver.swift
struct Resolver {
func items(ctx: Context, _: NoArguments) async throws -> [Item] {
try await Item.query(on: ctx.req.d).with(\.$user).all()
}
}
```

```swift Item+GraphQL.swift
extension Item {
func owner(_: Context, _: NoArguments) async -> User? {
return $user
}
}
```

##
Whether it is a better option is up to you and your use cases, but do keep in mind that GraphQL promotes the avoidance of overfetching.
9 changes: 4 additions & 5 deletions Documentation/guides/features/graphql-ide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
icon: squirrel
order: 50
order: 40
---

# GraphQL IDE
Expand All @@ -13,7 +13,7 @@ Pioneer will disable any GraphQL IDE automatically regardless of the specified p

## GraphiQL

GraphiQL is the official GraphQL IDE by the GraphQL Foundation. The current GraphiQL version has met feature parody with [GraphQL Playground](#graphql-playground) (*mostly).
GraphiQL is the official GraphQL IDE by the GraphQL Foundation. The current GraphiQL version has met feature parody with [GraphQL Playground](#graphql-playground) (\*mostly).

![](/static/graphiql.png)

Expand Down Expand Up @@ -76,7 +76,7 @@ GET /playground # (For playground)
Apollo Sandbox is a cloud hosted in browser GraphQL IDE developed by Apollo GraphQL and their choice of replacement for [GraphQL Playground](#graphql-playground). Apollo Sandbox provide all features available in [GraphQL Playground](#graphql-playground) and a lot more. However, this is either:

- A cloud based solution that require CORS configuration and cannot be self-hosted, or
- A locally embedded solution that limited capabilities compared to the cloud version.
- A locally embedded solution that limited capabilities compared to the cloud version.

Both solutions is not open source.

Expand All @@ -102,7 +102,7 @@ server.applyMiddleware(on: app)

<sub>You can also just set this up on your own</sub>

+++ Embedded Locally
+++ Embedded Locally

Embedded version of Apollo Sandbox served similarly to [GraphiQL](#graphiql) without needing to setup CORS.

Expand All @@ -123,7 +123,6 @@ Given that, the preffered / default option for `apolloSandbox` is the redirect o

+++


Afterwards, you can go to [./playground](http://localhost:8080/playground) to open a instance of Apollo Sandbox whether it is the cloud or the locally embedded version.

## Banana Cake Pop
Expand Down
5 changes: 3 additions & 2 deletions Documentation/guides/features/graphql-over-http.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
icon: file-binary
order: 80
order: 70
---

# GraphQL Over HTTP
Expand Down Expand Up @@ -41,7 +41,7 @@ import Vapor

let app = try Application(.detect())

@Sendable
@Sendable
func getContext(req: Request, res: Response) -> Context {
// Do something extra if needed
Context(req: req, res: req)
Expand All @@ -67,6 +67,7 @@ By default if you don't provide a seperate context builder for websocket, Pionee

==- Custom Request for Websocket
The custom request will similar to the request used to upgrade to websocket but will have:

- The headers taken from `"header"/"headers"` value from the `ConnectionParams` or all the entirety of `ConnectionParams`
- The query parameters taken from `"query"/"queries"/"queryParams"/"queryParameters"` value from the `ConnectionParams`
- The body from the `GraphQLRequest`
Expand Down
Loading