Skip to content

Commit

Permalink
Merge pull request 99designs#3 from sothychan/federation
Browse files Browse the repository at this point in the history
Pulled in v0.10 and fixed merge conflicts
  • Loading branch information
marwan-at-work committed Nov 12, 2019
2 parents 63e6214 + 90fa467 commit 8fb9130
Show file tree
Hide file tree
Showing 97 changed files with 3,618 additions and 1,325 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ linters:
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck

Expand Down
49 changes: 30 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

[gqlgen](https://github.com/99designs/gqlgen) is a Go library for building GraphQL servers without any fuss. gqlgen is:

- **Schema first** — Define your API using the GraphQL [Schema Definition Language](http://graphql.org/learn/schema/).
- **Type safe** — You should never see `map[string]interface{}` here.
- **Codegen** — Let us generate the boring bits, so you can build your app quickly.
- **Schema first** — Define your API using the GraphQL [Schema Definition Language](http://graphql.org/learn/schema/).
- **Type safe** — You should never see `map[string]interface{}` here.
- **Codegen** — Let us generate the boring bits, so you can build your app quickly.

[Feature Comparison](https://gqlgen.com/feature-comparison/)

Expand All @@ -29,32 +29,40 @@ Read our [Contribution Guidelines](https://github.com/99designs/gqlgen/blob/mast
### How do I prevent fetching child objects that might not be used?

When you have nested or recursive schema like this:

```graphql
type User {
id: ID!
name: String!
friends: [User!]!
id: ID!
name: String!
friends: [User!]!
}
```

You need to tell gqlgen that we should only fetch friends if the user requested it. There are two ways to do this.

1. Write the model yourself and leave off friends.
#### Custom Models

Write a custom model that omits the Friends model:

```go
type User struct {
Id int
Name string
ID int
Name string
}
```

And reference the model in `gqlgen.yml`:

```yaml
# gqlgen.yml
models:
User:
model: github.com/you/pkg/model.User # go import path to the User struct above
```

2. Keep using the generated model, and mark the field as requiring a resolver explicitly
#### Explicit Resolvers

If you want to Keep using the generated model: mark the field as requiring a resolver explicitly in `gqlgen.yml`:

```yaml
# gqlgen.yml
Expand All @@ -66,33 +74,36 @@ models:
```

After doing either of the above and running generate we will need to provide a resolver for friends:

```go
func (r *userResolver) Friends(ctx context.Context, obj *User) ([]*User, error) {
// select * from user where friendid = obj.ID
return friends, nil
// select * from user where friendid = obj.ID
return friends, nil
}
```

### IDs are strings but I like ints, why cant I have ints?

You can by remapping it in config:

```yaml
models:
ID: # The GraphQL type ID is backed by
model:
- github.com/99designs/gqlgen/graphql.IntID # An go integer
- github.com/99designs/gqlgen/graphql.ID # or a go string
- github.com/99designs/gqlgen/graphql.IntID # An go integer
- github.com/99designs/gqlgen/graphql.ID # or a go string
```

This means gqlgen will be able to automatically bind to strings or ints for models you have written yourself, but the
first model in this list is used as the default type and it will always be used when:
- generating models based on schema
- as arguments in resolvers

- Generating models based on schema
- As arguments in resolvers

There isnt any way around this, gqlgen has no way to know what you want in a given context.

## Other Resources

- [Christopher Biscardi @ Gophercon UK 2018](https://youtu.be/FdURVezcdcw)
- [Introducing gqlgen: a GraphQL Server Generator for Go](https://99designs.com.au/blog/engineering/gqlgen-a-graphql-server-generator-for-go/)
- [Dive into GraphQL by Iván Corrales Solera](https://medium.com/@ivan.corrales.solera/dive-into-graphql-9bfedf22e1a)
- [Christopher Biscardi @ Gophercon UK 2018](https://youtu.be/FdURVezcdcw)
- [Introducing gqlgen: a GraphQL Server Generator for Go](https://99designs.com.au/blog/engineering/gqlgen-a-graphql-server-generator-for-go/)
- [Dive into GraphQL by Iván Corrales Solera](https://medium.com/@ivan.corrales.solera/dive-into-graphql-9bfedf22e1a)
6 changes: 5 additions & 1 deletion api/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ func Generate(cfg *config.Config, option ...Option) error {
// Merge again now that the generated models have been injected into the typemap
data, err := codegen.BuildData(cfg, schemaMutators)
if err != nil {
return errors.Wrap(err, "merging failed")
return errors.Wrap(err, "merging type systems failed")
}

if err = codegen.GenerateCode(data); err != nil {
return errors.Wrap(err, "generating code failed")
}

for _, p := range plugins {
Expand Down
161 changes: 78 additions & 83 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// client is used internally for testing. See readme for alternatives

package client

import (
Expand All @@ -7,82 +8,63 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"

"github.com/mitchellh/mapstructure"
)

// Client for graphql requests
type Client struct {
url string
client *http.Client
}

// New creates a graphql client
func New(url string, client ...*http.Client) *Client {
p := &Client{
url: url,
type (
// Client used for testing GraphQL servers. Not for production use.
Client struct {
h http.Handler
opts []Option
}

if len(client) > 0 {
p.client = client[0]
} else {
p.client = http.DefaultClient
// Option implements a visitor that mutates an outgoing GraphQL request
//
// This is the Option pattern - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
Option func(bd *Request)

// Request represents an outgoing GraphQL request
Request struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
HTTP *http.Request `json:"-"`
}
return p
}

type Request struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
}

type Option func(r *Request)

func Var(name string, value interface{}) Option {
return func(r *Request) {
if r.Variables == nil {
r.Variables = map[string]interface{}{}
}

r.Variables[name] = value
// Response is a GraphQL layer response from a handler.
Response struct {
Data interface{}
Errors json.RawMessage
Extensions map[string]interface{}
}
}
)

func Operation(name string) Option {
return func(r *Request) {
r.OperationName = name
// New creates a graphql client
// Options can be set that should be applied to all requests made with this client
func New(h http.Handler, opts ...Option) *Client {
p := &Client{
h: h,
opts: opts,
}

return p
}

// MustPost is a convenience wrapper around Post that automatically panics on error
func (p *Client) MustPost(query string, response interface{}, options ...Option) {
if err := p.Post(query, response, options...); err != nil {
panic(err)
}
}

func (p *Client) mkRequest(query string, options ...Option) Request {
r := Request{
Query: query,
}

for _, option := range options {
option(&r)
}

return r
}

type ResponseData struct {
Data interface{}
Errors json.RawMessage
Extensions map[string]interface{}
}

func (p *Client) Post(query string, response interface{}, options ...Option) (resperr error) {
respDataRaw, resperr := p.RawPost(query, options...)
if resperr != nil {
return resperr
// Post sends a http POST request to the graphql endpoint with the given query then unpacks
// the response into the given object.
func (p *Client) Post(query string, response interface{}, options ...Option) error {
respDataRaw, err := p.RawPost(query, options...)
if err != nil {
return err
}

// we want to unpack even if there is an error, so we can see partial responses
Expand All @@ -94,48 +76,61 @@ func (p *Client) Post(query string, response interface{}, options ...Option) (re
return unpackErr
}

func (p *Client) RawPost(query string, options ...Option) (*ResponseData, error) {
r := p.mkRequest(query, options...)
requestBody, err := json.Marshal(r)
// RawPost is similar to Post, except it skips decoding the raw json response
// unpacked onto Response. This is used to test extension keys which are not
// available when using Post.
func (p *Client) RawPost(query string, options ...Option) (*Response, error) {
r, err := p.newRequest(query, options...)
if err != nil {
return nil, fmt.Errorf("encode: %s", err.Error())
return nil, fmt.Errorf("build: %s", err.Error())
}

rawResponse, err := p.client.Post(p.url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("post: %s", err.Error())
}
defer func() {
_ = rawResponse.Body.Close()
}()

if rawResponse.StatusCode >= http.StatusBadRequest {
responseBody, _ := ioutil.ReadAll(rawResponse.Body)
return nil, fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody)
}
w := httptest.NewRecorder()
p.h.ServeHTTP(w, r)

responseBody, err := ioutil.ReadAll(rawResponse.Body)
if err != nil {
return nil, fmt.Errorf("read: %s", err.Error())
if w.Code >= http.StatusBadRequest {
return nil, fmt.Errorf("http %d: %s", w.Code, w.Body.String())
}

// decode it into map string first, let mapstructure do the final decode
// because it can be much stricter about unknown fields.
respDataRaw := &ResponseData{}
err = json.Unmarshal(responseBody, &respDataRaw)
respDataRaw := &Response{}
err = json.Unmarshal(w.Body.Bytes(), &respDataRaw)
if err != nil {
return nil, fmt.Errorf("decode: %s", err.Error())
}

return respDataRaw, nil
}

type RawJsonError struct {
json.RawMessage
}
func (p *Client) newRequest(query string, options ...Option) (*http.Request, error) {
bd := &Request{
Query: query,
HTTP: httptest.NewRequest(http.MethodPost, "/", nil),
}
bd.HTTP.Header.Set("Content-Type", "application/json")

// per client options from client.New apply first
for _, option := range p.opts {
option(bd)
}
// per request options
for _, option := range options {
option(bd)
}

switch bd.HTTP.Header.Get("Content-Type") {
case "application/json":
requestBody, err := json.Marshal(bd)
if err != nil {
return nil, fmt.Errorf("encode: %s", err.Error())
}
bd.HTTP.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
default:
panic("unsupported encoding" + bd.HTTP.Header.Get("Content-Type"))
}

func (r RawJsonError) Error() string {
return string(r.RawMessage)
return bd.HTTP, nil
}

func unpack(data interface{}, into interface{}) error {
Expand Down
Loading

0 comments on commit 8fb9130

Please sign in to comment.