Preface: This is a fork of https://github.com/shurcooL/graphql
with extended features (subscription client, named operation)
The subscription client follows Apollo client specification https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md, using websocket protocol with https://github.com/nhooyr/websocket, a minimal and idiomatic WebSocket library for Go.
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.
Status: In active early research and development. The API will change when opportunities for improvement are discovered; it is not yet frozen.
go-graphql-client
requires Go version 1.13 or later.
go get -u github.com/hasura/go-graphql-client
Construct a GraphQL client, specifying the GraphQL server URL. Then, you can use it to make GraphQL queries and mutations.
client := graphql.NewClient("https://example.com/graphql", nil)
// Use client...
Some GraphQL servers may require authentication. The graphql
package does not directly handle authentication. Instead, when creating a new client, you're expected to pass an http.Client
that performs authentication. The easiest and recommended way to do this is to use the golang.org/x/oauth2
package. You'll need an OAuth token with the right scopes. Then:
import "golang.org/x/oauth2"
func main() {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: os.Getenv("GRAPHQL_TOKEN")},
)
httpClient := oauth2.NewClient(context.Background(), src)
client := graphql.NewClient("https://example.com/graphql", httpClient)
// Use client...
To make a GraphQL query, you need to define a corresponding Go type.
For example, to make the following GraphQL query:
query {
me {
name
}
}
You can define this variable:
var query struct {
Me struct {
Name graphql.String
}
}
Then call client.Query
, passing a pointer to it:
err := client.Query(context.Background(), &query, nil)
if err != nil {
// Handle error.
}
fmt.Println(query.Me.Name)
// Output: Luke Skywalker
Often, you'll want to specify arguments on some fields. You can use the graphql
struct field tag for this.
For example, to make the following GraphQL query:
{
human(id: "1000") {
name
height(unit: METER)
}
}
You can define this variable:
var q struct {
Human struct {
Name graphql.String
Height graphql.Float `graphql:"height(unit: METER)"`
} `graphql:"human(id: \"1000\")"`
}
Then call client.Query
:
err := client.Query(context.Background(), &q, nil)
if err != nil {
// Handle error.
}
fmt.Println(q.Human.Name)
fmt.Println(q.Human.Height)
// Output:
// Luke Skywalker
// 1.72
However, that'll only work if the arguments are constant and known in advance. Otherwise, you will need to make use of variables. Replace the constants in the struct field tag with variable names:
var q struct {
Human struct {
Name graphql.String
Height graphql.Float `graphql:"height(unit: $unit)"`
} `graphql:"human(id: $id)"`
}
Then, define a variables
map with their values:
variables := map[string]interface{}{
"id": graphql.ID(id),
"unit": starwars.LengthUnit("METER"),
}
Finally, call client.Query
providing variables
:
err := client.Query(context.Background(), &q, variables)
if err != nil {
// Handle error.
}
Some GraphQL queries contain inline fragments. You can use the graphql
struct field tag to express them.
For example, to make the following GraphQL query:
{
hero(episode: "JEDI") {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
You can define this variable:
var q struct {
Hero struct {
Name graphql.String
Droid struct {
PrimaryFunction graphql.String
} `graphql:"... on Droid"`
Human struct {
Height graphql.Float
} `graphql:"... on Human"`
} `graphql:"hero(episode: \"JEDI\")"`
}
Alternatively, you can define the struct types corresponding to inline fragments, and use them as embedded fields in your query:
type (
DroidFragment struct {
PrimaryFunction graphql.String
}
HumanFragment struct {
Height graphql.Float
}
)
var q struct {
Hero struct {
Name graphql.String
DroidFragment `graphql:"... on Droid"`
HumanFragment `graphql:"... on Human"`
} `graphql:"hero(episode: \"JEDI\")"`
}
Then call client.Query
:
err := client.Query(context.Background(), &q, nil)
if err != nil {
// Handle error.
}
fmt.Println(q.Hero.Name)
fmt.Println(q.Hero.PrimaryFunction)
fmt.Println(q.Hero.Height)
// Output:
// R2-D2
// Astromech
// 0
Mutations often require information that you can only find out by performing a query first. Let's suppose you've already done that.
For example, to make the following GraphQL mutation:
mutation($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
variables {
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
You can define:
var m struct {
CreateReview struct {
Stars graphql.Int
Commentary graphql.String
} `graphql:"createReview(episode: $ep, review: $review)"`
}
variables := map[string]interface{}{
"ep": starwars.Episode("JEDI"),
"review": starwars.ReviewInput{
Stars: graphql.Int(5),
Commentary: graphql.String("This is a great movie!"),
},
}
Then call client.Mutate
:
err := client.Mutate(context.Background(), &m, variables)
if err != nil {
// Handle error.
}
fmt.Printf("Created a %v star review: %v\n", m.CreateReview.Stars, m.CreateReview.Commentary)
// Output:
// Created a 5 star review: This is a great movie!
Sometimes, you don't need any fields returned from a mutation. Doing that is easy.
For example, to make the following GraphQL mutation:
mutation($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review)
}
variables {
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
You can define:
var m struct {
CreateReview string `graphql:"createReview(episode: $ep, review: $review)"`
}
variables := map[string]interface{}{
"ep": starwars.Episode("JEDI"),
"review": starwars.ReviewInput{
Stars: graphql.Int(5),
Commentary: graphql.String("This is a great movie!"),
},
}
Then call client.Mutate
:
err := client.Mutate(context.Background(), &m, variables)
if err != nil {
// Handle error.
}
fmt.Printf("Created a review: %s.\n", m.CreateReview)
// Output:
// Created a review: .
Construct a Subscription client, specifying the GraphQL server URL.
client := graphql.NewSubscriptionClient("wss://example.com/graphql")
defer client.Close()
// Subscribe subscriptions
// ...
// finally run the client
client.Run()
To make a GraphQL subscription, you need to define a corresponding Go type.
For example, to make the following GraphQL query:
subscription {
me {
name
}
}
You can define this variable:
var subscription struct {
Me struct {
Name graphql.String
}
}
Then call client.Subscribe
, passing a pointer to it:
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue *json.RawMessage, errValue error) error {
if errValue != nil {
// handle error
// if returns error, it will failback to `onError` event
return nil
}
data := query{}
err := json.Unmarshal(dataValue, &data)
fmt.Println(query.Me.Name)
// Output: Luke Skywalker
})
if err != nil {
// Handle error.
}
// you can unsubscribe the subscription while the client is running
client.Unsubscribe(subscriptionId)
The subscription client is authenticated with GraphQL server through connection params:
client := graphql.NewSubscriptionClient("wss://example.com/graphql").
WithConnectionParams(map[string]interface{}{
"headers": map[string]string{
"authentication": "...",
},
})
client.
// write timeout of websocket client
WithTimeout(time.Minute).
// When the websocket server was stopped, the client will retry connecting every second until timeout
WithRetryTimeout(time.Minute).
// sets loging function to print out received messages. By default, nothing is printed
WithLog(log.Println).
// max size of response message
WithReadLimit(10*1024*1024).
// these operation event logs won't be printed
WithoutLogTypes(graphql.GQL_DATA, graphql.GQL_CONNECTION_KEEP_ALIVE)
// OnConnected event is triggered when the websocket connected to GraphQL server sucessfully
client.OnConnected(fn func())
// OnDisconnected event is triggered when the websocket server was stil down after retry timeout
client.OnDisconnected(fn func())
// OnConnected event is triggered when there is any connection error. This is bottom exception handler level
// If this function is empty, or returns nil, the error is ignored
// If returns error, the websocket connection will be terminated
client.OnError(onError func(sc *SubscriptionClient, err error) error)
By default the subscription client uses nhooyr WebSocket client. If you need to customize the client, or prefer using Gorilla WebSocket, let's follow the Websocket interface and replace the constructor with WithWebSocket
method:
// WebsocketHandler abstracts WebSocket connection functions
// ReadJSON and WriteJSON data of a frame from the WebSocket connection.
// Close the WebSocket connection.
type WebsocketConn interface {
ReadJSON(v interface{}) error
WriteJSON(v interface{}) error
Close() error
// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a
// message exceeds the limit, the connection sends a close message to the peer
// and returns ErrReadLimit to the application.
SetReadLimit(limit int64)
}
// WithWebSocket replaces customized websocket client constructor
func (sc *SubscriptionClient) WithWebSocket(fn func(sc *SubscriptionClient) (WebsocketConn, error)) *SubscriptionClient
Example
// the default websocket constructor
func newWebsocketConn(sc *SubscriptionClient) (WebsocketConn, error) {
options := &websocket.DialOptions{
Subprotocols: []string{"graphql-ws"},
}
c, _, err := websocket.Dial(sc.GetContext(), sc.GetURL(), options)
if err != nil {
return nil, err
}
// The default WebsocketHandler implementation using nhooyr's
return &WebsocketHandler{
ctx: sc.GetContext(),
Conn: c,
timeout: sc.GetTimeout(),
}, nil
}
client := graphql.NewSubscriptionClient("wss://example.com/graphql")
defer client.Close()
client.WithWebSocket(newWebsocketConn)
client.Run()
There are extensible parts in the GraphQL query that we sometimes use. They are optional so that we shouldn't required them in the method. To make it flexible, we can abstract these options as optional arguments that follow this interface.
type Option interface {
Type() OptionType
String() string
}
client.Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error
Currently we support 2 option types: operation_name
and operation_directive
. The operation name option is built-in because it is unique. We can use the option directly with OperationName
// query MyQuery {
// ...
// }
client.Query(ctx, &q, variables, graphql.OperationName("MyQuery"))
In contrast, operation directive is various and customizable on different GraphQL servers. There isn't any built-in directive in the library. You need to define yourself. For example:
// define @cached directive for Hasura queries
// https://hasura.io/docs/latest/graphql/cloud/response-caching.html#enable-caching
type cachedDirective struct {
ttl int
}
func (cd cachedDirective) Type() OptionType {
// operation_directive
return graphql.OptionTypeOperationDirective
}
func (cd cachedDirective) String() string {
if cd.ttl <= 0 {
return "@cached"
}
return fmt.Sprintf("@cached(ttl: %d)", cd.ttl)
}
// query MyQuery @cached {
// ...
// }
client.Query(ctx, &q, variables, graphql.OperationName("MyQuery"), cachedDirective{})
Operation name is still on API decision plan shurcooL#12. However, in my opinion separate methods are easier choice to avoid breaking changes
func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error
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)
In the case we developers want to decode JSON response ourself. Moreover, the default UnmarshalGraphQL
function isn't ideal with complicated nested interfaces
func (c *Client) QueryRaw(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{}) (*json.RawMessage, error)
func (c *Client) NamedQueryRaw(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{}) (*json.RawMessage, error)
Path | Synopsis |
---|---|
example/graphqldev | graphqldev is a test program currently being used for developing graphql package. |
ident | Package ident provides functions for parsing and converting identifier names between various naming convention. |
internal/jsonutil | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. |