Skip to content

Commit

Permalink
add ExecRaw and change the output type of Raw methods (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
hgiasac authored Aug 9, 2022
1 parent d9ce49d commit 93707b1
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 47 deletions.
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu

**Status:** In active early research and development. The API will change when opportunities for improvement are discovered; it is not yet frozen.

**Note**: Before v0.8.0, `QueryRaw`, `MutateRaw` and `Subscribe` methods return `*json.RawMessage`. This output type is redundant to be decoded. From v0.8.0, the output type is changed to `[]byte`.

- [go-graphql-client](#go-graphql-client)
- [Installation](#installation)
- [Usage](#usage)
Expand Down Expand Up @@ -479,7 +481,7 @@ var subscription struct {
Then call `client.Subscribe`, passing a pointer to it:
```Go
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue *json.RawMessage, errValue error) error {
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue []byte, errValue error) error {
if errValue != nil {
// handle error
// if returns error, it will failback to `onError` event
Expand All @@ -504,7 +506,7 @@ if err != nil {
You can programmatically stop the subscription while the client is running by using the `Unsubscribe` method, or returning a special error to stop it in the callback.
```Go
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue *json.RawMessage, errValue error) error {
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue []byte, errValue error) error {
// ...
// return this error to stop the subscription in the callback
return graphql.ErrSubscriptionStopped
Expand Down Expand Up @@ -700,7 +702,7 @@ if err := client.Exec(ctx, query, &res, map[string]any{}); err != nil {
}

subscription := "subscription{something(where: {" + strings.Join(filters, ", ") + "}){id}}"
subscriptionId, err := subscriptionClient.Exec(subscription, nil, func(dataValue *json.RawMessage, errValue error) error {
subscriptionId, err := subscriptionClient.Exec(subscription, nil, func(dataValue []byte, errValue error) error {
if errValue != nil {
// handle error
// if returns error, it will failback to `onError` event
Expand All @@ -712,6 +714,22 @@ subscriptionId, err := subscriptionClient.Exec(subscription, nil, func(dataValue
})
```
If you prefer decoding JSON yourself, use `ExecRaw` instead.
```Go
query := `query{something(where: { foo: { _eq: "bar" }}){id}}`
var res struct {
Somethings []Something `json:"something"`
}

raw, err := client.ExecRaw(ctx, query, map[string]any{})
if err != nil {
panic(err)
}

err = json.Unmarshal(raw, &res)
```
### With operation name (deprecated)
Operation name is still on API decision plan https://github.com/shurcooL/graphql/issues/12. However, in my opinion separate methods are easier choice to avoid breaking changes
Expand All @@ -721,21 +739,21 @@ func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, var

func (c *Client) NamedMutate(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error

func (sc *SubscriptionClient) NamedSubscribe(name string, v interface{}, variables map[string]interface{}, handler func(message *json.RawMessage, err error) error) (string, error)
func (sc *SubscriptionClient) NamedSubscribe(name string, v interface{}, variables map[string]interface{}, handler func(message []byte, err error) error) (string, error)
```
### Raw bytes response
In the case we developers want to decode JSON response ourself. Moreover, the default `UnmarshalGraphQL` function isn't ideal with complicated nested interfaces
```Go
func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}) (*json.RawMessage, error)
func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}) ([]byte, error)

func (c *Client) MutateRaw(ctx context.Context, q interface{}, variables map[string]interface{}) (*json.RawMessage, error)
func (c *Client) MutateRaw(ctx context.Context, q interface{}, variables map[string]interface{}) ([]byte, error)

func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}) (*json.RawMessage, error)
func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}) ([]byte, error)

func (c *Client) NamedMutateRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}) (*json.RawMessage, error)
func (c *Client) NamedMutateRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}) ([]byte, error)
```
### Multiple mutations with ordered map
Expand Down
6 changes: 2 additions & 4 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// Package graphql provides a GraphQL client implementation.
//
// For more information, see package github.com/shurcooL/githubv4,
// which is a specialized version targeting GitHub GraphQL API v4.
// That package is driving the feature development.
// For more information, see package github.com/hasura/go-graphql-client
//
// Status: In active early research and development. The API will change when
// opportunities for improvement are discovered; it is not yet frozen.
//
// For now, see README for more details.
package graphql // import "github.com/shurcooL/graphql"
package graphql
5 changes: 2 additions & 3 deletions example/subscription/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
Expand Down Expand Up @@ -46,7 +45,7 @@ func startSubscription() error {
} `graphql:"helloSaid"`
}

subId, err := client.Subscribe(sub, nil, func(data *json.RawMessage, err error) error {
subId, err := client.Subscribe(sub, nil, func(data []byte, err error) error {

if err != nil {
log.Println(err)
Expand All @@ -56,7 +55,7 @@ func startSubscription() error {
if data == nil {
return nil
}
log.Println(string(*data))
log.Println(string(data))
return nil
})

Expand Down
39 changes: 27 additions & 12 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,32 +71,32 @@ func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, va
// with a query derived from q, populating the response into it.
// q should be a pointer to struct that corresponds to the GraphQL schema.
// return raw bytes message.
func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, error) {
func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
return c.doRaw(ctx, queryOperation, q, variables, options...)
}

// NamedQueryRaw executes a single GraphQL query request, with operation name
// return raw bytes message.
func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, error) {
func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
return c.doRaw(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
}

// MutateRaw executes a single GraphQL mutation request,
// with a mutation derived from m, populating the response into it.
// m should be a pointer to struct that corresponds to the GraphQL schema.
// return raw bytes message.
func (c *Client) MutateRaw(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, error) {
func (c *Client) MutateRaw(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
return c.doRaw(ctx, mutationOperation, m, variables, options...)
}

// NamedMutateRaw executes a single GraphQL mutation request, with operation name
// return raw bytes message.
func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, error) {
func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
return c.doRaw(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
}

// buildAndRequest the common method that builds and send graphql request
func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, *http.Response, io.Reader, Errors) {
func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) {
var query string
var err error
switch op {
Expand All @@ -114,7 +114,7 @@ func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interf
}

// Request the common method that send graphql request
func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options ...Option) (*json.RawMessage, *http.Response, io.Reader, Errors) {
func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) {
in := struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
Expand Down Expand Up @@ -210,22 +210,27 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
return nil, nil, nil, Errors{we}
}

var rawData []byte
if out.Data != nil && len(*out.Data) > 0 {
rawData = []byte(*out.Data)
}

if len(out.Errors) > 0 {
if c.debug && (out.Errors[0].Extensions == nil || out.Errors[0].Extensions["request"] == nil) {
out.Errors[0] = out.Errors[0].
withRequest(request, reqReader).
withResponse(resp, respReader)
}

return out.Data, resp, respReader, out.Errors
return rawData, resp, respReader, out.Errors
}

return out.Data, resp, respReader, nil
return rawData, resp, respReader, nil
}

// do executes a single GraphQL operation.
// return raw message and error
func (c *Client) doRaw(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) (*json.RawMessage, error) {
func (c *Client) doRaw(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
data, _, _, err := c.buildAndRequest(ctx, op, v, variables, options...)
if len(err) > 0 {
return data, err
Expand All @@ -246,9 +251,19 @@ func (c *Client) Exec(ctx context.Context, query string, v interface{}, variable
return c.processResponse(v, data, resp, respBuf, errs)
}

func (c *Client) processResponse(v interface{}, data *json.RawMessage, resp *http.Response, respBuf io.Reader, errs Errors) error {
if data != nil {
err := jsonutil.UnmarshalGraphQL(*data, v)
// Executes a pre-built query and returns the raw json message. Unlike the Query method you have to specify in the query the
// fields that you want to receive as they are not inferred from the interface. This method is useful if you need to build the query dynamically.
func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, error) {
data, _, _, errs := c.request(ctx, query, variables, options...)
if len(errs) > 0 {
return data, errs
}
return data, nil
}

func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response, respBuf io.Reader, errs Errors) error {
if len(data) > 0 {
err := jsonutil.UnmarshalGraphQL(data, v)
if err != nil {
we := newError(ErrGraphQLDecode, err)
if c.debug {
Expand Down
99 changes: 99 additions & 0 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,105 @@ func TestClient_Query_ignoreFields(t *testing.T) {
}
}

// Test raw json response from query
func TestClient_Query_RawResponse(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
body := mustRead(req.Body)
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
t.Errorf("got body: %v, want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}}`)
})
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})

var q struct {
User struct {
ID string `graphql:"id"`
Name string `graphql:"name"`
}
}
rawBytes, err := client.QueryRaw(context.Background(), &q, map[string]interface{}{})
if err != nil {
t.Fatal(err)
}

err = json.Unmarshal(rawBytes, &q)
if err != nil {
t.Fatal(err)
}

if got, want := q.User.Name, "Gopher"; got != want {
t.Errorf("got q.User.Name: %q, want: %q", got, want)
}
}

// Test exec pre-built query
func TestClient_Exec_Query(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
body := mustRead(req.Body)
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
t.Errorf("got body: %v, want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}}`)
})
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})

var q struct {
User struct {
ID string `graphql:"id"`
Name string `graphql:"name"`
}
}

err := client.Exec(context.Background(), "{user{id,name}}", &q, map[string]interface{}{})
if err != nil {
t.Fatal(err)
}

if got, want := q.User.Name, "Gopher"; got != want {
t.Errorf("got q.User.Name: %q, want: %q", got, want)
}
}

// Test exec pre-built query, return raw json string
func TestClient_Exec_QueryRaw(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
body := mustRead(req.Body)
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
t.Errorf("got body: %v, want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}}`)
})
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})

var q struct {
User struct {
ID string `graphql:"id"`
Name string `graphql:"name"`
}
}

rawBytes, err := client.ExecRaw(context.Background(), "{user{id,name}}", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}

err = json.Unmarshal(rawBytes, &q)
if err != nil {
t.Fatal(err)
}

if got, want := q.User.Name, "Gopher"; got != want {
t.Errorf("got q.User.Name: %q, want: %q", got, want)
}
}

// localRoundTripper is an http.RoundTripper that executes HTTP transactions
// by using handler directly, instead of going over an HTTP connection.
type localRoundTripper struct {
Expand Down
Loading

0 comments on commit 93707b1

Please sign in to comment.