这是indexloc提供的服务,不要输入任何密码
Skip to content

introduce ExecRaw and change the output type of Raw methods #44

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

Merged
merged 1 commit into from
Aug 9, 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
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