Files
genqlient/graphql/client.go
T
Ben Kraft 4d2058c617 Add a bunch more documentation (and rewrite README) (#86)
## Summary:
In this commit, I add a bunch more documentation of how to use
genqlient, including a getting started guide, and a FAQ that also acts
as a how-to shaped index of common configuration options and extension
points.  Much of this is adapted from the internal doc I wrote at Khan,
or reorganized from what was in the README.  Speaking of which, now that
there are better places for all the details, I rewrote the README to be
a bit more of an overview and index of other documentation.  (For now I
left the "unmaintained" notes, which I'll remove fairly soon once all
our ducks are in a row.)

Fixes #26, #39.

Issue: https://github.com/Khan/genqlient/issues/26
Issue: https://github.com/Khan/genqlient/issues/39

## Test plan:
make check, read the docs


Author: benjaminjkraft

Reviewers: mahtabsabet, benjaminjkraft, aberkan, dnerdy, jvoll, MiguelCastillo

Required Reviewers: 

Approved By: mahtabsabet

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint

Pull Request URL: https://github.com/Khan/genqlient/pull/86
2021-09-14 11:05:11 -07:00

133 lines
3.7 KiB
Go

package graphql
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// Client is the interface that the generated code calls into to actually make
// requests.
//
// Unstable: This interface is likely to change before v1.0, see #19. Creating
// a client with NewClient will remain the same.
type Client interface {
// MakeRequest must make a request to the client's GraphQL API.
//
// ctx is the context that should be used to make this request. If context
// is disabled in the genqlient settings, this will be set to
// context.Background().
//
// query is the literal string representing the GraphQL query, e.g.
// `query myQuery { myField }`. variables contains the GraphQL variables
// to be sent along with the query, or may be nil if there are none.
// Typically, GraphQL APIs will accept a JSON payload of the form
// {"query": "query myQuery { ... }", "variables": {...}}`
// but MakeRequest may use some other transport, handle extensions, or set
// other parameters, if it wishes.
//
// retval is a pointer to the struct representing the query result, e.g.
// new(myQueryResponse). Typically, GraphQL APIs will return a JSON
// payload of the form
// {"data": {...}, "errors": {...}}
// and retval is designed so that `data` will json-unmarshal into `retval`.
// (Errors are returned.) But again, MakeRequest may customize this.
MakeRequest(
ctx context.Context,
opName string,
query string,
retval interface{},
variables map[string]interface{},
) error
}
type client struct {
httpClient *http.Client
endpoint string
method string
}
// NewClient returns a Client which makes requests to the given endpoint,
// suitable for most users.
//
// The client makes POST requests to the given GraphQL endpoint using standard
// GraphQL HTTP-over-JSON transport. It will use the given http client, or
// http.DefaultClient if a nil client is passed.
//
// The typical method of adding authentication headers is to wrap the client's
// Transport to add those headers. See example/caller.go for an example.
func NewClient(endpoint string, httpClient *http.Client) Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &client{httpClient, endpoint, http.MethodPost}
}
type payload struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
// OpName is only required if there are multiple queries in the document,
// but we set it unconditionally, because that's easier.
OpName string `json:"operationName"`
}
type response struct {
Data interface{} `json:"data"`
Errors gqlerror.List `json:"errors"`
}
func (c *client) MakeRequest(ctx context.Context, opName string, query string, retval interface{}, variables map[string]interface{}) error {
body, err := json.Marshal(payload{
Query: query,
Variables: variables,
OpName: opName,
})
if err != nil {
return err
}
req, err := http.NewRequest(
c.method,
c.endpoint,
bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if ctx != nil {
req = req.WithContext(ctx)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var respBody []byte
respBody, err = ioutil.ReadAll(resp.Body)
if err != nil {
respBody = []byte(fmt.Sprintf("<unreadable: %v>", err))
}
return fmt.Errorf("returned error %v: %s", resp.Status, respBody)
}
var dataAndErrors response
dataAndErrors.Data = retval
err = json.NewDecoder(resp.Body).Decode(&dataAndErrors)
if err != nil {
return err
}
if len(dataAndErrors.Errors) > 0 {
return dataAndErrors.Errors
}
return nil
}