You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

382 lines
11 KiB
Go

package graphql
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/hasura/go-graphql-client/internal/jsonutil"
)
// This function allows you to tweak the HTTP request. It might be useful to set authentication
// headers amongst other things
type RequestModifier func(*http.Request)
// Client is a GraphQL client.
type Client struct {
url string // GraphQL server URL.
httpClient *http.Client
requestModifier RequestModifier
debug bool
}
// NewClient creates a GraphQL client targeting the specified GraphQL server URL.
// If httpClient is nil, then http.DefaultClient is used.
func NewClient(url string, httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{
url: url,
httpClient: httpClient,
requestModifier: nil,
}
}
// Query executes a single GraphQL query request,
// with a query derived from q, populating the response into it.
// q should be a pointer to struct that corresponds to the GraphQL schema.
func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error {
return c.do(ctx, queryOperation, q, variables, options...)
}
// NamedQuery executes a single GraphQL query request, with operation name
//
// Deprecated: this is the shortcut of Query method, with NewOperationName option
func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) error {
return c.do(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
}
// Mutate 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.
func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) error {
return c.do(ctx, mutationOperation, m, variables, options...)
}
// NamedMutate executes a single GraphQL mutation request, with operation name
//
// Deprecated: this is the shortcut of Mutate method, with NewOperationName option
func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) error {
return c.do(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
}
// Query executes a single GraphQL query request,
// 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) {
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) {
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) {
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) {
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) {
var query string
var err error
switch op {
case queryOperation:
query, err = ConstructQuery(v, variables, options...)
case mutationOperation:
query, err = ConstructMutation(v, variables, options...)
}
if err != nil {
return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
}
in := struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
}{
Query: query,
Variables: variables,
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(in)
if err != nil {
return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
}
reqReader := bytes.NewReader(buf.Bytes())
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, reqReader)
if err != nil {
e := newError(ErrRequestError, fmt.Errorf("problem constructing request: %w", err))
if c.debug {
e = e.withRequest(request, reqReader)
}
return nil, nil, nil, Errors{e}
}
request.Header.Add("Content-Type", "application/json")
if c.requestModifier != nil {
c.requestModifier(request)
}
resp, err := c.httpClient.Do(request)
if c.debug {
reqReader.Seek(0, io.SeekStart)
}
if err != nil {
e := newError(ErrRequestError, err)
if c.debug {
e = e.withRequest(request, reqReader)
}
return nil, nil, nil, Errors{e}
}
defer resp.Body.Close()
r := resp.Body
if resp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(r)
if err != nil {
return nil, nil, nil, Errors{newError(ErrJsonDecode, fmt.Errorf("problem trying to create gzip reader: %w", err))}
}
defer gr.Close()
r = gr
}
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
err := newError(ErrRequestError, fmt.Errorf("%v; body: %q", resp.Status, body))
if c.debug {
err = err.withRequest(request, reqReader)
}
return nil, nil, nil, Errors{err}
}
var out struct {
Data *json.RawMessage
Errors Errors
}
// copy the response reader for debugging
var respReader *bytes.Reader
if c.debug {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, nil, Errors{newError(ErrJsonDecode, err)}
}
respReader = bytes.NewReader(body)
r = io.NopCloser(respReader)
}
err = json.NewDecoder(r).Decode(&out)
if c.debug {
respReader.Seek(0, io.SeekStart)
}
if err != nil {
we := newError(ErrJsonDecode, err)
if c.debug {
we = we.withRequest(request, reqReader).
withResponse(resp, respReader)
}
return nil, nil, nil, Errors{we}
}
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 out.Data, 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) {
data, _, _, err := c.buildAndRequest(ctx, op, v, variables, options...)
if len(err) > 0 {
return data, err
}
return data, nil
}
// do executes a single GraphQL operation and unmarshal json.
func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) error {
data, resp, respBuf, errs := c.buildAndRequest(ctx, op, v, variables, options...)
if data != nil {
err := jsonutil.UnmarshalGraphQL(*data, v)
if err != nil {
we := newError(ErrGraphQLDecode, err)
if c.debug {
we = we.withResponse(resp, respBuf)
}
errs = append(errs, we)
}
}
if len(errs) > 0 {
return errs
}
return nil
}
// Returns a copy of the client with the request modifier set. This allows you to reuse the same
// TCP connection for multiple slightly different requests to the same server
// (i.e. different authentication headers for multitenant applications)
func (c *Client) WithRequestModifier(f RequestModifier) *Client {
return &Client{
url: c.url,
httpClient: c.httpClient,
requestModifier: f,
}
}
// WithDebug enable debug mode to print internal error detail
func (c *Client) WithDebug(debug bool) *Client {
return &Client{
url: c.url,
httpClient: c.httpClient,
requestModifier: c.requestModifier,
debug: debug,
}
}
// errors represents the "errors" array in a response from a GraphQL server.
// If returned via error interface, the slice is expected to contain at least 1 element.
//
// Specification: https://facebook.github.io/graphql/#sec-Errors.
type Errors []Error
type Error struct {
Message string `json:"message"`
Extensions map[string]interface{} `json:"extensions"`
Locations []struct {
Line int `json:"line"`
Column int `json:"column"`
} `json:"locations"`
}
// Error implements error interface.
func (e Error) Error() string {
return fmt.Sprintf("Message: %s, Locations: %+v", e.Message, e.Locations)
}
// Error implements error interface.
func (e Errors) Error() string {
b := strings.Builder{}
for _, err := range e {
b.WriteString(err.Error())
}
return b.String()
}
func (e Error) getInternalExtension() map[string]interface{} {
if e.Extensions == nil {
return make(map[string]interface{})
}
if ex, ok := e.Extensions["internal"]; ok {
return ex.(map[string]interface{})
}
return make(map[string]interface{})
}
func newError(code string, err error) Error {
return Error{
Message: err.Error(),
Extensions: map[string]interface{}{
"code": code,
},
}
}
func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error {
internal := e.getInternalExtension()
bodyBytes, err := ioutil.ReadAll(bodyReader)
if err != nil {
internal["error"] = err
} else {
internal["request"] = map[string]interface{}{
"headers": req.Header,
"body": string(bodyBytes),
}
}
if e.Extensions == nil {
e.Extensions = make(map[string]interface{})
}
e.Extensions["internal"] = internal
return e
}
func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error {
internal := e.getInternalExtension()
bodyBytes, err := ioutil.ReadAll(bodyReader)
if err != nil {
internal["error"] = err
} else {
internal["response"] = map[string]interface{}{
"headers": res.Header,
"body": string(bodyBytes),
}
}
e.Extensions["internal"] = internal
return e
}
// UnmarshalGraphQL parses the JSON-encoded GraphQL response data and stores
// the result in the GraphQL query data structure pointed to by v.
//
// The implementation is created on top of the JSON tokenizer available
// in "encoding/json".Decoder.
// This function is re-exported from the internal package
func UnmarshalGraphQL(data []byte, v interface{}) error {
return jsonutil.UnmarshalGraphQL(data, v)
}
type operationType uint8
const (
queryOperation operationType = iota
mutationOperation
// subscriptionOperation // Unused.
ErrRequestError = "request_error"
ErrJsonEncode = "json_encode_error"
ErrJsonDecode = "json_decode_error"
ErrGraphQLEncode = "graphql_encode_error"
ErrGraphQLDecode = "graphql_decode_error"
)