Skip to content

Commit

Permalink
Merge pull request #334 from 99designs/support-response-extensions
Browse files Browse the repository at this point in the history
Support Extensions in Response
  • Loading branch information
Mathew Byrne authored Sep 17, 2018
2 parents bfb48f2 + 8fdf4fb commit 2bd1cc2
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 67 deletions.
44 changes: 28 additions & 16 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,49 +73,61 @@ func (p *Client) mkRequest(query string, options ...Option) Request {
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
}

// we want to unpack even if there is an error, so we can see partial responses
unpackErr := unpack(respDataRaw.Data, response)

if respDataRaw.Errors != nil {
return RawJsonError{respDataRaw.Errors}
}
return unpackErr
}

func (p *Client) RawPost(query string, options ...Option) (*ResponseData, error) {
r := p.mkRequest(query, options...)
requestBody, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("encode: %s", err.Error())
return nil, fmt.Errorf("encode: %s", err.Error())
}

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

if rawResponse.StatusCode >= http.StatusBadRequest {
responseBody, _ := ioutil.ReadAll(rawResponse.Body)
return fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody)
return nil, fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody)
}

responseBody, err := ioutil.ReadAll(rawResponse.Body)
if err != nil {
return fmt.Errorf("read: %s", err.Error())
return nil, fmt.Errorf("read: %s", err.Error())
}

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

// we want to unpack even if there is an error, so we can see partial responses
unpackErr := unpack(respDataRaw.Data, response)

if respDataRaw.Errors != nil {
return RawJsonError{respDataRaw.Errors}
}
return unpackErr
return respDataRaw, nil
}

type RawJsonError struct {
Expand Down
2 changes: 1 addition & 1 deletion codegen/templates/data.go

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions codegen/templates/generated.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ func (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinitio
})

return &graphql.Response{
Data: buf,
Errors: ec.Errors,
}
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions, }
{{- else }}
return graphql.ErrorResponse(ctx, "queries are not supported")
{{- end }}
Expand All @@ -146,8 +146,9 @@ func (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefini
})

return &graphql.Response{
Data: buf,
Errors: ec.Errors,
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions,
}
{{- else }}
return graphql.ErrorResponse(ctx, "mutations are not supported")
Expand Down Expand Up @@ -181,8 +182,9 @@ func (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDe
}

return &graphql.Response{
Data: buf,
Errors: ec.Errors,
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions,
}
}
{{- else }}
Expand Down
11 changes: 6 additions & 5 deletions codegen/testserver/generated.go

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

21 changes: 21 additions & 0 deletions codegen/testserver/generated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"testing"
"time"

"github.com/99designs/gqlgen/graphql"

"github.com/99designs/gqlgen/client"
"github.com/99designs/gqlgen/handler"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -110,6 +112,25 @@ func TestGeneratedServer(t *testing.T) {
})
}

func TestResponseExtension(t *testing.T) {
srv := httptest.NewServer(handler.GraphQL(
NewExecutableSchema(Config{
Resolvers: &testResolver{},
}),
handler.RequestMiddleware(func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
rctx := graphql.GetRequestContext(ctx)
if err := rctx.RegisterExtension("example", "value"); err != nil {
panic(err)
}
return next(ctx)
}),
))
c := client.New(srv.URL)

raw, _ := c.RawPost(`query { valid }`)
require.Equal(t, raw.Extensions["example"], "value")
}

type testResolver struct {
tick chan string
}
Expand Down
16 changes: 9 additions & 7 deletions example/chat/generated.go

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

11 changes: 6 additions & 5 deletions example/config/generated.go

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

6 changes: 3 additions & 3 deletions example/dataloader/generated.go

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

6 changes: 3 additions & 3 deletions example/scalars/generated.go

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

6 changes: 3 additions & 3 deletions example/selection/generated.go

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

11 changes: 6 additions & 5 deletions example/starwars/generated.go

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

11 changes: 6 additions & 5 deletions example/todo/generated.go

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

23 changes: 21 additions & 2 deletions graphql/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ type RequestContext struct {
DirectiveMiddleware FieldMiddleware
RequestMiddleware RequestMiddleware

errorsMu sync.Mutex
Errors gqlerror.List
errorsMu sync.Mutex
Errors gqlerror.List
extensionsMu sync.Mutex
Extensions map[string]interface{}
}

func DefaultResolverMiddleware(ctx context.Context, next Resolver) (res interface{}, err error) {
Expand Down Expand Up @@ -176,3 +178,20 @@ func AddError(ctx context.Context, err error) {
func AddErrorf(ctx context.Context, format string, args ...interface{}) {
GetRequestContext(ctx).Errorf(ctx, format, args...)
}

// RegisterExtension registers an extension, returns error if extension has already been registered
func (c *RequestContext) RegisterExtension(key string, value interface{}) error {
c.extensionsMu.Lock()
defer c.extensionsMu.Unlock()

if c.Extensions == nil {
c.Extensions = make(map[string]interface{})
}

if _, ok := c.Extensions[key]; ok {
return fmt.Errorf("extension already registered for key %s", key)
}

c.Extensions[key] = value
return nil
}
5 changes: 3 additions & 2 deletions graphql/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
)

type Response struct {
Data json.RawMessage `json:"data"`
Errors gqlerror.List `json:"errors,omitempty"`
Data json.RawMessage `json:"data"`
Errors gqlerror.List `json:"errors,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}

func ErrorResponse(ctx context.Context, messagef string, args ...interface{}) *Response {
Expand Down
6 changes: 3 additions & 3 deletions integration/generated.go

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

0 comments on commit 2bd1cc2

Please sign in to comment.