go-graphql-client

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.
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
.
Installation
go-graphql-client
requires Go version 1.16 or later. For older Go versions, downgrade the library version below v0.7.1.
go get -u github.com/hasura/go-graphql-client
Usage
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)
Authentication
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)
Simple Query
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 string
}
}
Then call client.Query
, passing a pointer to it:
err := client.Query(context.Background(), &query, nil)
if err != nil {
}
fmt.Println(query.Me.Name)
Arguments and Variables
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 string
Height float64 `graphql:"height(unit: METER)"`
} `graphql:"human(id: \"1000\")"`
}
Then call client.Query
:
err := client.Query(context.Background(), &q, nil)
if err != nil {
}
fmt.Println(q.Human.Name)
fmt.Println(q.Human.Height)
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 string
Height float64 `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 {
}
Custom scalar tag
Because the generator reflects recursively struct objects, it can't know if the struct is a custom scalar such as JSON. To avoid expansion of the field during query generation, let's add the tag scalar:"true"
to the custom scalar. If the scalar implements the JSON decoder interface, it will be automatically decoded.
struct {
Viewer struct {
ID interface{}
Login string
CreatedAt time.Time
DatabaseID int
}
}
struct {
Viewer struct {
ID interface{}
Login string
CreatedAt time.Time
DatabaseID int
} `scalar:"true"`
}
Skip GraphQL field
struct {
Viewer struct {
ID interface{} `graphql:"-"`
Login string
CreatedAt time.Time `graphql:"-"`
DatabaseID int
}
}
Inline Fragments
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 string
Droid struct {
PrimaryFunction string
} `graphql:"... on Droid"`
Human struct {
Height float64
} `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 string
}
HumanFragment struct {
Height float64
}
)
var q struct {
Hero struct {
Name 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 {
}
fmt.Println(q.Hero.Name)
fmt.Println(q.Hero.PrimaryFunction)
fmt.Println(q.Hero.Height)
Specify GraphQL type name
The GraphQL type is automatically inferred from Go type by reflection. However, it's cumbersome in some use cases, e.g lowercase names. In Go, a type name with a first lowercase letter is considered private. If we need to reuse it for other packages, there are 2 approaches: type alias or implement GetGraphQLType
method.
type UserReviewInput struct {
Review string
UserID string
}
type user_review_input UserReviewInput
func (u UserReviewInput) GetGraphQLType() string { return "user_review_input" }
variables := map[string]interface{}{
"input": UserReviewInput{}
}
Mutations
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 int
Commentary string
} `graphql:"createReview(episode: $ep, review: $review)"`
}
variables := map[string]interface{}{
"ep": starwars.Episode("JEDI"),
"review": starwars.ReviewInput{
Stars: 5,
Commentary: "This is a great movie!",
},
}
Then call client.Mutate
:
err := client.Mutate(context.Background(), &m, variables)
if err != nil {
}
fmt.Printf("Created a %v star review: %v\n", m.CreateReview.Stars, m.CreateReview.Commentary)
Mutations Without Fields
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: 5,
Commentary: "This is a great movie!",
},
}
Then call client.Mutate
:
err := client.Mutate(context.Background(), &m, variables)
if err != nil {
}
fmt.Printf("Created a review: %s.\n", m.CreateReview)
Subscription
Usage
Construct a Subscription client, specifying the GraphQL server URL.
client := graphql.NewSubscriptionClient("wss://example.com/graphql")
defer client.Close()
client.Run()
Subscribe
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 string
}
}
Then call client.Subscribe
, passing a pointer to it:
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue []byte, errValue error) error {
if errValue != nil {
return nil
}
data := query{}
err := json.Unmarshal(dataValue, &data)
fmt.Println(query.Me.Name)
return nil
})
if err != nil {
}
Stop the subscription
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.
subscriptionId, err := client.Subscribe(&query, nil, func(dataValue []byte, errValue error) error {
return graphql.ErrSubscriptionStopped
})
if err != nil {
}
client.Unsubscribe(subscriptionId)
Authentication
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": "...",
},
})
Options
client.
WithTimeout(time.Minute).
WithRetryTimeout(time.Minute).
WithLog(log.Println).
WithReadLimit(10*1024*1024).
WithoutLogTypes(graphql.GQL_DATA, graphql.GQL_CONNECTION_KEEP_ALIVE)
Events
client.OnConnected(fn func())
client.OnDisconnected(fn func())
client.OnError(onError func(sc *SubscriptionClient, err error) error)
Custom HTTP Client
Use WithWebSocketOptions
to customize the HTTP client which is used by the subscription client.
client.WithWebSocketOptions(WebsocketOptions{
HTTPClient: &http.Client{
Transport: http.DefaultTransport,
Timeout: time.Minute,
}
})
Custom WebSocket client
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:
type WebsocketConn interface {
ReadJSON(v interface{}) error
WriteJSON(v interface{}) error
Close() error
SetReadLimit(limit int64)
}
func (sc *SubscriptionClient) WithWebSocket(fn func(sc *SubscriptionClient) (WebsocketConn, error)) *SubscriptionClient
Example
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
}
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()
Options
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
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:
type cachedDirective struct {
ttl int
}
func (cd cachedDirective) Type() OptionType {
return graphql.OptionTypeOperationDirective
}
func (cd cachedDirective) String() string {
if cd.ttl <= 0 {
return "@cached"
}
return fmt.Sprintf("@cached(ttl: %d)", cd.ttl)
}
client.Query(ctx, &q, variables, graphql.OperationName("MyQuery"), cachedDirective{})
Execute pre-built query
The Exec
function allows you to executing pre-built queries. While using reflection to build queries is convenient as you get some resemblance of type safety, it gets very cumbersome when you need to create queries semi-dynamically. For instance, imagine you are building a CLI tool to query data from a graphql endpoint and you want users to be able to narrow down the query by passing cli flags or something.
filters := []string{
`fieldA: {subfieldA: {_eq: "a"}}`,
`fieldB: {_eq: "b"}`,
...
}
query := "query{something(where: {" + strings.Join(filters, ", ") + "}){id}}"
res := struct {
Somethings []Something
}{}
if err := client.Exec(ctx, query, &res, map[string]any{}); err != nil {
panic(err)
}
subscription := "subscription{something(where: {" + strings.Join(filters, ", ") + "}){id}}"
subscriptionId, err := subscriptionClient.Exec(subscription, nil, func(dataValue []byte, errValue error) error {
if errValue != nil {
return nil
}
data := query{}
err := json.Unmarshal(dataValue, &data)
})
If you prefer decoding JSON yourself, use ExecRaw
instead.
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
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 []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
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{}) ([]byte, 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{}) ([]byte, error)
Multiple mutations with ordered map
You might need to make multiple mutations in single query. It's not very convenient with structs
so you can use ordered map [][2]interface{}
instead.
For example, to make the following GraphQL mutation:
mutation($login1: String!, $login2: String!, $login3: String!) {
createUser(login: $login1) { login }
createUser(login: $login2) { login }
createUser(login: $login3) { login }
}
variables {
"login1": "grihabor",
"login2": "diman",
"login3": "indigo"
}
You can define:
type CreateUser struct {
Login string
}
m := [][2]interface{}{
{"createUser(login: $login1)", &CreateUser{}},
{"createUser(login: $login2)", &CreateUser{}},
{"createUser(login: $login3)", &CreateUser{}},
}
variables := map[string]interface{}{
"login1": "grihabor",
"login2": "diman",
"login3": "indigo",
}
Debugging and Unit test
Enable debug mode with the WithDebug
function. If the request is failed, the request and response information will be included in extensions[].internal
property.
{
"errors": [
{
"message":"Field 'user' is missing required arguments: login",
"extensions": {
"internal": {
"request": {
"body":"{\"query\":\"{user{name}}\"}",
"headers": {
"Content-Type": ["application/json"]
}
},
"response": {
"body":"{\"errors\": [{\"message\": \"Field 'user' is missing required arguments: login\",\"locations\": [{\"line\": 7,\"column\": 3}]}]}",
"headers": {
"Content-Type": ["application/json"]
}
}
}
},
"locations": [
{
"line":7,
"column":3
}
]
}
]
}
Because the GraphQL query string is generated in runtime using reflection, it isn't really safe. To assure the GraphQL query is expected, it's necessary to write some unit test for query construction.
func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error)
func ConstructMutation(v interface{}, variables map[string]interface{}, options ...Option) (string, error)
func ConstructSubscription(v interface{}, variables map[string]interface{}, options ...Option) (string, error)
func UnmarshalGraphQL(data []byte, v interface{}) error
Directories
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. |
References
License