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

Federation docs and examples #1018

Merged
merged 1 commit into from
Feb 8, 2020
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
2 changes: 1 addition & 1 deletion .circleci/check-coverage
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
set -euo pipefail
go get github.com/mattn/goveralls

go test -coverprofile=/tmp/coverage.out -coverpkg=./... $(go list github.com/99designs/gqlgen/... | grep -v server)
go test -coverprofile=/tmp/coverage.out -coverpkg=./... $(go list github.com/99designs/gqlgen/... | grep -v example)
goveralls -coverprofile=/tmp/coverage.out -service=circle-ci -repotoken=$REPOTOKEN -ignore='example/*/*,example/*/*/*,integration/*,integration/*/*,codegen/testserver/*'
13 changes: 13 additions & 0 deletions .circleci/check-federation
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -euo pipefail

cd example/federation

./start.sh &

sleep 10

echo "### running jest integration spec"
./node_modules/.bin/jest --color

11 changes: 11 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ jobs:
- run: cd integration ; npm install
- run: .circleci/check-integration

federation:
docker:
- image: alpine:3.10
steps:
- checkout
- run: apk add --no-cache --no-progress nodejs npm go musl-dev git bash
- run: go mod download
- run: cd example/federation ; npm install
- run: .circleci/check-federation

workflows:
version: 2
build_and_test:
Expand All @@ -44,4 +54,5 @@ workflows:
- test
- cover
- integration
- federation

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
/integration/node_modules
/integration/schema-fetched.graphql
/example/chat/package-lock.json
/example/federation/package-lock.json
/example/federation/node_modules
/codegen/gen
/gen

Expand Down
187 changes: 187 additions & 0 deletions docs/content/recipes/federation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
title: 'Using Apollo federation gqlgen'
description: How federate many services into a single graph using Apollo
linkTitle: Apollo Federation
menu: { main: { parent: 'recipes' } }
---

In this quick guide we are going to implement the example [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/)
server in gqlgen. You can find the finished result in the [examples directory](/example/federation).


## Create the federated servers

For each server to be federated we will create a new gqlgen project.

```bash
go run github.com/99designs/gqlgen
```

Update the schema to reflect the federated example
```graphql
type Review {
body: String
author: User @provides(fields: "username")
product: Product
}

type User @extends @key(fields: "id") {
id: ID! @external
reviews: [Review]
}

type Product @extends @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}
```

> Note
>
> gqlgen doesnt currently support `extend type Foo` syntax for apollo federation, we must use
> the `@extends` directive instead.


and regenerate
```bash
go run github.com/99designs/gqlgen
```

then implement the resolvers
```go
// These two methods are required for gqlgen to resolve the internal id-only wrapper structs.
// This boilerplate might be removed in a future version of gqlgen that can no-op id only nodes.
func (r *entityResolver) FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) {
return &model.Product{
Upc: upc,
}, nil
}

func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*model.User, error) {
return &model.User{
ID: id,
}, nil
}

// Here we implement the stitched part of this service, returning reviews for a product. Of course normally you would
// go back to the database, but we are just making some data up here.
func (r *productResolver) Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) {
switch obj.Upc {
case "top-1":
return []*model.Review{{
Body: "A highly effective form of birth control.",
}}, nil

case "top-2":
return []*model.Review{{
Body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.",
}}, nil

case "top-3":
return []*model.Review{{
Body: "This is the last straw. Hat you will wear. 11/10",
}}, nil

}
return nil, nil
}

func (r *userResolver) Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) {
if obj.ID == "1234" {
return []*model.Review{{
Body: "Has an odd fascination with hats.",
}}, nil
}
return nil, nil
}
```

> Note
>
> Repeat this step for each of the services in the apollo doc (accounts, products, reviews)

## Create the federation gateway

```bash
npm install --save @apollo/gateway apollo-server graphql
```

```typescript
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require("@apollo/gateway");

const gateway = new ApolloGateway({
serviceList: [
{ name: 'accounts', url: 'http://localhost:4001/query' },
{ name: 'products', url: 'http://localhost:4002/query' },
{ name: 'reviews', url: 'http://localhost:4003/query' }
],
});

const server = new ApolloServer({
gateway,

subscriptions: false,
});

server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```

## Start all the services

In separate terminals:
```bash
go run accounts/server.go
go run products/server.go
go run reviews/server.go
node gateway/index.js
```

## Query the federated gateway

The examples from the apollo doc should all work, eg

```graphql
query {
me {
username
reviews {
body
product {
name
upc
}
}
}
}
```

should return

```json
{
"data": {
"me": {
"username": "Me",
"reviews": [
{
"body": "A highly effective form of birth control.",
"product": {
"name": "Trilby",
"upc": "top-1"
}
},
{
"body": "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.",
"product": {
"name": "Trilby",
"upc": "top-1"
}
}
]
}
}
}
```
55 changes: 55 additions & 0 deletions example/federation/accounts/gqlgen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls

# Where should the generated server code go?
exec:
filename: graph/generated/generated.go
package: generated

federation:
filename: graph/generated/federation.go
package: generated

# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model

# Where should the resolver implementations go?
resolver:
layout: follow-schema
dir: graph
package: graph

# Optional: turn on use `gqlgen:"fieldName"` tags in your models
# struct_tag: json

# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false

# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true

# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "github.com/99designs/gqlgen/example/federation/accounts/graph/model"

# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
26 changes: 26 additions & 0 deletions example/federation/accounts/graph/entity.resolvers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
package graph

import (
"context"

"github.com/99designs/gqlgen/example/federation/accounts/graph/generated"
"github.com/99designs/gqlgen/example/federation/accounts/graph/model"
)

func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*model.User, error) {
name := "User " + id
if id == "1234" {
name = "Me"
}

return &model.User{
ID: id,
Username: name,
}, nil
}

func (r *Resolver) Entity() generated.EntityResolver { return &entityResolver{r} }

type entityResolver struct{ *Resolver }
57 changes: 57 additions & 0 deletions example/federation/accounts/graph/generated/federation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading