Categorygithub.com/runtimeracer/go-graphql-client
modulepackage
0.2.4
Repository: https://github.com/runtimeracer/go-graphql-client.git
Documentation: pkg.go.dev

# README

go-graphql-client

Build Status GoDoc

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.

Installation

go-graphql-client requires Go version 1.13 or later.

go get -u github.com/runtimeracer/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)
// Use client...

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)
	// Use client...

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 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

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   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.
}

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  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

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!

Subcriptions

Usage

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()

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 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)

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.
	//  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)

Events

// 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)

With operation name

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 *json.RawMessage, 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{}) (*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)

Directories

PathSynopsis
example/graphqldevgraphqldev is a test program currently being used for developing graphql package.
identPackage ident provides functions for parsing and converting identifier names between various naming convention.
internal/jsonutilPackage jsonutil provides a function for decoding JSON into a GraphQL query data structure.

References

License

# Packages

No description provided by the author
Package ident provides functions for parsing and converting identifier names between various naming convention.

# Functions

NewBoolean is a helper to make a new *Boolean.
NewClient creates a GraphQL client targeting the specified GraphQL server URL.
NewFloat is a helper to make a new *Float.
NewID is a helper to make a new *ID.
NewInt is a helper to make a new *Int.
NewString is a helper to make a new *String.
No description provided by the author
NewToken is a helper to make a new *String.

# Constants

Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the specific operation.
The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server accepted the connection.
The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server rejected the connection.
Client sends this message after plain websocket connection to start the communication with the server.
Server message that should be sent right after each GQL_CONNECTION_ACK processed and then periodically to keep the client connection alive.
Client sends this message to terminate the connection.
The server sends this message to transfter the GraphQL execution result from the server to the client, this message is a response for GQL_START message.
Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation errors (resolver errors are part of GQL_DATA message, and will be added as errors array).
Internal status, for logging only.
Client sends this message to execute GraphQL operation.
Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe).
Unknown operation type, for logging only.

# Structs

Client is a GraphQL client.
No description provided by the author
SubscriptionClient is a GraphQL subscription client.

# Interfaces

No description provided by the author
WebsocketHandler abstracts WebSocket connecton functions ReadJSON and WriteJSON data of a frame from the WebSocket connection.

# Type aliases

No description provided by the author
No description provided by the author
No description provided by the author
OperationMessageType.
No description provided by the author
No description provided by the author