Coder Social home page Coder Social logo

go-graphql-client's Introduction

go-graphql-client

Unit tests

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.

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.20 or later. For older Go versions:

  • >= 1.16 < 1.20: downgrade the library to version v0.9.x
  • < 1.16: 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)
// 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. Variable names must be upper case, see here

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 {
	// 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   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 {
	// 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   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 {
	// Handle error.
}

Variables get encoded as normal json. So if you supply a struct for a variable and want to rename fields, you can do this like that:

type Dimensions struct {
	Width int `json:"ship_width"`,
	Height int `json:"ship_height"`
}

var myDimensions = Dimensions{
	Width : 10,
	Height : 6,
}

var mutation struct {
  CreateDimensions struct {
     ID string `graphql:"id"`
  } `graphql:"create_dimensions(ship_dimensions: $ship_dimensions)"`
}

variables := map[string]interface{}{
	"ship_dimensions":  myDimensions,
}

err := client.Mutate(context.TODO(), &mutation, variables)

which will set ship_dimensions to an object with the properties ship_width and ship_height.

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

// Output:
// {
//   viewer {
//	   id
//		 login
//		 createdAt
//		 databaseId
//   }
// }

struct {
	Viewer struct {
		ID         interface{}
		Login      string
		CreatedAt  time.Time
		DatabaseID int
	} `scalar:"true"`
}

// Output
// { viewer }

Skip GraphQL field

struct {
  Viewer struct {
		ID         interface{} `graphql:"-"`
		Login      string
		CreatedAt  time.Time `graphql:"-"`
		DatabaseID int
  }
}

// Output
// {viewer{login,databaseId}}

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 {
	// Handle error.
}
fmt.Println(q.Hero.Name)
fmt.Println(q.Hero.PrimaryFunction)
fmt.Println(q.Hero.Height)

// Output:
// R2-D2
// Astromech
// 0

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 alias
type user_review_input UserReviewInput
// or implement GetGraphQLType method
func (u UserReviewInput) GetGraphQLType() string { return "user_review_input" }

variables := map[string]interface{}{
  "input": UserReviewInput{}
}

//query arguments without GetGraphQLType() defined
//($input: UserReviewInput!)
//query arguments with GetGraphQLType() defined:w
//($input: user_review_input!)

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 {
	// 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!

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 {
	// Handle error.
}
fmt.Printf("Created a review: %s.\n", m.CreateReview)

// Output:
// Created a review: .

Subscription

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 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 {
		// handle error
		// if returns error, it will failback to `onError` event
		return nil
	}
	data := query{}
	// use the github.com/hasura/go-graphql-client/pkg/jsonutil package
	err := jsonutil.UnmarshalGraphQL(dataValue, &data)

	fmt.Println(query.Me.Name)

	// Output: Luke Skywalker
	return nil
})

if err != nil {
	// Handle error.
}

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 this error to stop the subscription in the callback
	return graphql.ErrSubscriptionStopped
})

if err != nil {
	// Handle error.
}

// unsubscribe the subscription while the client is running with the subscription ID
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": "...",
		},
	}).
	// or lazy parameters with function
  WithConnectionParamsFn(func () map[string]interface{} {
		return map[string]interface{} {
			"headers": map[string]string{
  				"authentication": "...",
  		},
		}
	})

Some servers validate custom auth tokens on the header instead. To authenticate with headers, use WebsocketOptions:

client := graphql.NewSubscriptionClient(serverEndpoint).
    WithWebSocketOptions(graphql.WebsocketOptions{
        HTTPHeader: http.Header{
            "Authorization": []string{"Bearer random-secret"},
        },
    })

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.GQLData, graphql.GQLConnectionKeepAlive).
	// the client should exit when all subscriptions were closed, default true
	WithExitWhenNoSubscription(false).
	// WithRetryStatusCodes allow retry the subscription connection when receiving one of these codes
	// the input parameter can be number string or range, e.g 4000-5000
	WithRetryStatusCodes("4000", "4000-4050").
	// WithSyncMode subscription messages are executed in sequence (without goroutine)
	WithSyncMode(true)

Subscription Protocols

The subscription client supports 2 protocols:

The protocol can be switchable by the WithProtocol function.

client.WithProtocol(graphql.GraphQLWS)

Handle connection error

GraphQL servers can define custom WebSocket error codes in the 3000-4999 range. For example, in the graphql-ws protocol, the server sends the invalid message error with status 4400. In this case, the subscription client should let the user handle the error through the OnError event.

client := graphql.NewSubscriptionClient(serverEndpoint).
  OnError(func(sc *graphql.SubscriptionClient, err error) error {
  	if strings.Contains(err.Error(), "invalid x-hasura-admin-secret/x-hasura-access-key") {
			// exit the subscription client due to unauthorized error
  		return err
  	}
		// otherwise ignore the error and the client continues to run
  	return nil
  })

Events

// OnConnected event is triggered when the websocket connected to GraphQL server sucessfully
client.OnConnected(fn func())

// OnDisconnected event is triggered when the websocket client was disconnected
client.OnDisconnected(fn func())

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

// OnConnectionAlive event is triggered when the websocket receive a connection alive message (differs per protocol)
client.OnConnectionAlive(fn func())

// OnSubscriptionComplete event is triggered when the subscription receives a terminated message from the server
client.OnSubscriptionComplete(fn func(sub Subscription))

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:

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

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

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

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 would be built dynamically somehow from the command line flags
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 {
		// handle error
		// if returns error, it will failback to `onError` event
		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 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 []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
				}
			]
		}
	]
}

For debugging queries, you can use Construct* functions to see what the generated query looks like:

// ConstructQuery build GraphQL query string from struct and variables
func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error)

// ConstructMutation build GraphQL mutation string from struct and variables
func ConstructMutation(v interface{}, variables map[string]interface{}, options ...Option) (string, error)

// ConstructSubscription build GraphQL subscription string from struct and variables
func ConstructSubscription(v interface{}, variables map[string]interface{}, options ...Option) (string, string, error)

// UnmarshalGraphQL parses the JSON-encoded GraphQL response data and stores
// the result in the GraphQL query data structure pointed to by v.
func UnmarshalGraphQL(data []byte, v interface{}) error

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.

Directories

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.

References

License

go-graphql-client's People

Contributors

airon-applyinnovations avatar bill-rich avatar calebbrown avatar cameronbrill avatar david-bain avatar davidbloss avatar davitovmasyan avatar dbarrosop avatar digitalcrab avatar dmitshur avatar dpulpeiro avatar grihabor avatar hgiasac avatar joshuadietz avatar kacperdrobny avatar kwapik avatar leighstillard avatar nico151999 avatar nizar-m avatar pontusntengnas avatar rafaelvanoni avatar schmidtw avatar senekis avatar sermojohn avatar shuheiktgw avatar soluchok avatar tangxusc avatar totalys avatar zdevaty avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

go-graphql-client's Issues

Error message indicating field that wasn't requested?

Hi there,

I'm trying to write a simple graphql integration with shopify to check prices and adjust them as needed. I've written up a query that looks like this:

query MyQuery {
		shop {
		  products(first: 10, query:"Rough Snowflake Camisole") {
			nodes {
			  id
			  descriptionHtml
			  title
			  totalVariants
			  variants(first: 10) {
				nodes {
				  id
				  sku
				  price
				}
			  }
			}
		  }
		}
	  }

And returns the output

{
  "data": {
    "shop": {
      "products": {
        "nodes": [
          {
            "id": "gid://shopify/Product/1974208299030",
            "descriptionHtml": "",
            "title": "Rough Snowflake Camisole",
            "totalVariants": 1,
            "variants": {
              "nodes": [
                {
                  "id": "gid://shopify/ProductVariant/19523123216406",
                  "sku": "",
                  "price": "77.00"
                }
              ]
            }
          }
        ]
      }
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 133,
      "actualQueryCost": 7,
      "throttleStatus": {
        "maximumAvailable": 1000,
        "currentlyAvailable": 993,
        "restoreRate": 50
      }
    }
  }
}

I've been working on converting that into using your library and have been getting a strange error message that looks like the following

[leigh@dreamsite shopify-go-graphql]$ go run .
2022/11/08 13:57:51 Message: Field 'nodes' doesn't exist on type 'ProductConnection', Locations: [{Line:1 Column:16}]
exit status 1
[leigh@dreamsite shopify-go-graphql]$

I can't track down what the reference to ProductConnection is. Here is the code I'm using, mildly sanitised:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	graphql "github.com/hasura/go-graphql-client"
	"github.com/shopspring/decimal"
)

type Decimal struct {
	decimal.Decimal
}

func main() {
	// Create a new HTTPClient
	client := graphql.NewClient("https://test.myshopify.com/admin/api/2021-07/graphql.json", nil)

	// Format our request with the graphql query and set the headers for authentication
	reqMod := graphql.RequestModifier(
		func(r *http.Request) {
			r.Header.Add("X-Shopify-Access-Token", "shpat_abc123")
			r.Header.Add("Content-Type", "application/graphql")
		})
	client = client.WithRequestModifier(reqMod)
	client = client.WithDebug(true)

	var ProductSearch struct {
		Shop struct {
			Products []struct {
				Nodes []struct {
					Id              int
					DescriptionHtml string
					Title           string
					TotalVariants   int
					Variants        []struct {
						Nodes []struct {
							Id    int
							Sku   string
							Price Decimal
						} `graphql:"nodes"`
					} `graphql:"variants"`
				} `graphql:"nodes"`
			} `graphql:"products"`
		} `graphql:"shop"`
	}

	// run it and capture the response
	err := client.Query(context.Background(), &ProductSearch, nil)
	//raw, err := client.ExecRaw(context.Background(), query, nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(ProductSearch)
}

I can't figure out for the life of me where ProductConnection is coming from. Can anyone point me in the right direction? I suspect I might be using a difficult way to build out my struct.

Edit: I've just figured out that the ProductConnection reference is a type from the shopify graphql API that I wasn't aware of. I should be able to figure out things from here I think, though I'm only just now learning reflection.

Edit2: Changing the data structure to this hasn't gotten me past this error:

type Product struct {
		Id              int
		DescriptionHtml string
		Title           string
		TotalVariants   int
		Variants        []struct {
			Id    int
			Sku   string
			Price Decimal
		}
	}

	type ProductConnection struct {
		Nodes []Product
	}

	var ProductSearch struct {
		Shop struct {
			Products ProductConnection
		}
	}```

Now it's:
```2022/11/08 14:45:54 Message: Field 'nodes' doesn't exist on type 'ProductConnection', Locations: [{Line:1 Column:16}]
exit status 1```

How to execute a GraphQL query that can not be translated into a struct?

For the context, I need to query data from this subgraph https://thegraph.com/hosted-service/subgraph/piavgh/uniswap-v3-arbitrum?selected=playground

HTTP endpoint is: https://api.thegraph.com/subgraphs/name/piavgh/uniswap-v3-arbitrum

Previously, I used github.com/machinebox/graphql, the query I used in the code is:

	req := graphql.NewRequest(fmt.Sprintf(`{
		pools(
			subgraphError: allow,
			where : {
				createdAtTimestamp_gte: %v
			},
			first: %v,
			skip: %v,
			orderBy: createdAtTimestamp,
			orderDirection: asc
		) {
			id
			liquidity
			sqrtPrice
			createdAtTimestamp
			tick
			feeTier
			token0 {
				id
				name
				symbol
			  	decimals
			}
			token1 {
				id
				name
				symbol
			  	decimals
			}
		}
	}`, lastCreatedAtTimestamp, first, skip),
	)

How can I translate this query into the kind of struct that hasura/go-graphql-client supports? Especially this part:

(
			subgraphError: allow,
			where : {
				createdAtTimestamp_gte: %v
			},
			first: %v,
			skip: %v,
			orderBy: createdAtTimestamp,
			orderDirection: asc
		)

Thanks

How are subscriptions handled on reconnect?

We have the problem that subscriptions are not re-established when the connection is reconnected. Looking at the code it seems if that is unexpected?
Looking into this in more detail I'm wondering if we're just seeing the retry timeout expire. Is it possible to retry without limit?

Mutation query issues

mutation query in graphql working fine
mutation createQuestion($data:QuestionInput!) { createQuestion(data: $data) { data{ id attributes{ title description question_id } } } }
mutation query in golang client
var respData struct { CreateQuestion struct { Data []struct { ID graphql.ID Attributes struct { Question_id graphql.String Title graphql.String Description graphql.String } } } }
passing data Like this
variables := map[string]interface{}{ "data": map[string]interface{}{ "title": "My testFirst FbPost", "description": "dfsdf is the body of my first post.", "question_id": "35005", }, }

It's gives Error when passing to server Strapi CMS server

GRAPHQL_PARSE_FAILED Syntax Error: Expected Name, found

Using date/time fields

Hi, I'm facing an issue when trying to pass time.Time values as variables during a query:

type Record struct {
	ID	string `graphql:"id"`
	CreatedAt time.Time `graphql:"created_at"`
}
type Query struct {
	Record []Record `graphql:"record(where:{created_at: {_gte: $gte, _lte: $lte}})"`
}

err := client.Query(context.Background(), &query, map[string]interface{}{
	"gte": time.Now(),
	"lte": time.Now(),
})

The following error shows up:

% go run main.go 
panic: Message: variable "gte" is declared as Time!, but used where timestamptz is expected, Locations: []

Same error occurs if I pass a string version of the date as gte. Wandering what's the appropriate way here.

Structs are not correctly supported to provide associated GraphQL type

Code:

package gqlclient

import (
	"fmt"

	"github.com/hasura/go-graphql-client"
)

// StringStringer is to support a built-in string type as a fmt.Stringer
type StringStringer string

func (s StringStringer) String() string { return string(s) }

type supportedType fmt.Stringer

type customTypeHint struct {
	data supportedType
	hint string
}

var _ graphql.GraphQLType = (*customTypeHint)(nil)
var _ fmt.Stringer = (*customTypeHint)(nil)

func NewCustomTypeHint(data supportedType, hintType string) graphql.GraphQLType {
	return &customTypeHint{
		data: data,
		hint: hintType,
	}
}

func (cth *customTypeHint) GetGraphQLType() string {
	return cth.hint
}

func (cth *customTypeHint) String() string {
	return cth.data.String()
}

Test:

package gqlclient

import (
	"testing"

	"github.com/hasura/go-graphql-client"
	"github.com/stretchr/testify/assert"
)

type gqlGetRowsQuery struct {
	GetRows struct {
		Data []struct {
			Id graphql.String
		}
	} `graphql:"getRows(batchId:$batchId)"`
}

func TestCustomTypeHint_GetGraphQLType(t *testing.T) {
	const hintType = "UUID"
	ct := NewCustomTypeHint(StringStringer("test"), hintType)
	assert.Equal(t, hintType, ct.GetGraphQLType())

	var query gqlGetRowsQuery
	hint := NewCustomTypeHint(
		StringStringer("9e573418-38f4-4a35-b3df-eb36c9bba2cd"),
		hintType,
	)
	queryVars := map[string]any{
		"batchId": hint,
	}
	constructQuery, err := graphql.ConstructQuery(&query, queryVars)
	assert.NoError(t, err)
	assert.Contains(t, constructQuery, hintType)
}

Test execution output:

=== RUN   TestCustomTypeHint_GetGraphQLType
    custom_type_hint_test.go:35: 
        	Error Trace:	/Users/abelan/dev/src/github.com/deputyapp/go-svc/pkg/gqlclient/custom_type_hint_test.go:33
        	Error:      	"query ($batchId:customTypeHint){getRows(batchId:$batchId){data{id}}}" does not contain "UUID"
        	Test:       	TestCustomTypeHint_GetGraphQLType
--- FAIL: TestCustomTypeHint_GetGraphQLType (0.00s)

Expected outcome: the test above passes.

Can't get graphql-ws protocol working

Here is the current code that I have

subscriptionClient = graphql.NewSubscriptionClient("ws://localhost:8888/graphql").
	WithConnectionParams(map[string]interface{}{
		"headers": map[string]string{
			"Authorization": "Bearer " + token,
		},
	}).
	WithProtocol(graphql.GraphQLWS).
	OnConnected(func() {
		log.Println("Connected")
	}).
	OnDisconnected(func() {
		log.Println("Disconnected")
	}).
	OnError(func(sc *graphql.SubscriptionClient, err error) error {
		log.Println(err.Error())
		return err
	})

_, err := subscriptionClient.Subscribe(query, nil, func(data []byte, errValue error) error {
	if errValue != nil {
		// handle error
		// if returns error, it will failback to `onError` even
		return errValue
	}

	// handle data here

	return nil
})

if err != nil {
	log.Fatal("Error when subscribe: ", err)
}

err = subscriptionClient.Run()

if err != nil {
	log.Fatal("Error Starting Subscription: ", err)
} else {
	log.Println("No error here")
	os.Exit(1)
}

When I tried to run this code, I got the error failed to read JSON message: failed to get reader: received close frame: status = StatusCode(4403) and reason = "Forbidden". But if I switched to the subscription transport protocol, it's working normally. I can assure the server side (not Hasura) is working because I have a NodeJS application subscribe to the server using graphql-ws protocol. Can someone give me some clue where I can try to debug this?

Module reports only `invalid syntax`

I have tried Query and QueryRaw and the error returned is "invalid syntax" I have added debug to the round tripper and I used withDebug nothing is logged. Any suggestions as I am at my wits ends and I likely need to switch modules as that's not enough to go on with no debug.

Add support for native go types

There was a comment in the base code (and in shurcooL's code) that it would be nice to support the native go types, so I implemented this feature.

#45

BUG: Mutation generates error

My mutation

mutation{
  addImage(input: {name: "test", namespace: "test-ns", imageStreamRef: "test-ns/test:latest", releaseVersion: "master"}) {
    numUids
  }
}

Error:

error="Message: Expected {, found <EOF>, Locations: [{Line:1 Column:9}]"

The above mutation is correct and its working. The problem is with the client.

I am only trying to do

if err := client.Mutate(context.Background(), &mutation, nil); err !=nil{
    return err
}

Casting pagination value as and ID but getting a string error

Below is an opaque string for the cursor value:
0x14000909420g3QAAAABZAACaWRtAAAAGjAxR0pCUDlYMzhZRktWRjMzMjQzTVM4Q1o2

However I get the following error when casting the value with graphql.ID().

Message: Variable $after of type ID! found as input to argument of type String.
exit status 1

My current workaround is to pass the after value in a predefined input and handle the value on the backend, however it would be nice to pass this relay opaque string as a cursor value.

Run a New Release

The docs show that jsonUtil is being exposed , but the latest release still has it in internal.

mutation query doesn't allow optional params

Hello!
I'm have this graphql tag
graphql:"createProducts(name: $name, url: $url, callbackUrl: $callbackUrl, email: $email)"
and this variables map

        vars := map[string]interface{}{
		"name": graphql.String(storefront),
		"url":      graphql.String(location),
	}

	email := config.email
	if len(email) > 0 {
		vars["email"] = graphql.String(email)
	}

	callbackUrl := config.CallbackUrl
	if len(callbackUrl) > 0 {
		vars["callbackUrl"] = graphql.String(callbackUrl)
	}

	if err := i.client.Mutate(ctx, mutation, vars); err != nil {
	        return err
	}

if config.CallbackUrl is empty, the result mutation is this, missing declare $callbackUrl as String

mutation (
  $email: String!
  $url: String!
  $name: String!
) {
  createProducts(
    name: $name
    url: $url
    email: $email
    callbackUrl: $callbackUrl
  ) {
    name
  }
}

if I change vars maps, adding "callbackUrl" as (*graphql.String)(nil), the variable declaration has the value $callbackUrl as String

vars := MutationVars{
		"name": graphql.String(storefront),
		"url":        graphql.String(location),
		"callbackUrl":    (*graphql.String)(nil),
	}

but the mutation return this error
"'callbackUrl' must be a string"

There is no way to create a variable in our mutation query and don't send it. Or is no dynamic way to change the graphql tag

bigint type is not handled

i believe we need to change the Int64 to be considered "bigint", unless there is some reason behind making all Int types be Int.

switch t.Kind() {
	case reflect.Slice, reflect.Array:
		// List. E.g., "[Int]".
		io.WriteString(w, "[")
		writeArgumentType(w, t.Elem(), nil, true)
		io.WriteString(w, "]")
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		io.WriteString(w, "Int")
	case reflect.Float32, reflect.Float64:
		io.WriteString(w, "Float")
	case reflect.Bool:
		io.WriteString(w, "Boolean")
	default:
		n := t.Name()
		if n == "string" {
			n = "String"
		}
		io.WriteString(w, n)
	}

like uuid used using type uuid string to send to hasura what to do for bigint

variable 'userId' is declared as 'Int', but used where 'bigint' is expected, Locations: [], Extensions: map[code:validation-failed path:$.selectionSet.insertUsersUserRolesOne.args.object.userId]
I tried this
1,

type bitgint int
variables := map[string]interface{}{
		"userId": bigint(newUser.InsertUsersUsersOne.Id),
		"role":   "USER",
	}

2,also I tried
HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES=true
then I sent userId as a string but still doesnot work

thanks

Nullable argument

I would like to pass a null argument as my GraphQL endpoint support null arguments. But it does not work.

My code:

type MyMutation struct {
  MyMutation struct {
	  Integer int
  } `graphql:"myMutation(input: {objectName: $objectName, variables: $variables})"`
}

parameters := map[string]interface{}{
  "objectName": "my-object",
  "variables": nil, // Here I indicate a null "value"
}
err := s.UserInput.graphqlClient.Mutate(context.Background(), &MyMutation, parameters)

But it panics at run time:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x78 pc=0x6a0177]

goroutine 1 [running]:
github.com/hasura/go-graphql-client.writeArgumentType({0x7c1040, 0xc00017e2d0}, {0x0, 0x0?}, {0x0, 0x0}, 0x1)
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/query.go:139 +0x57
github.com/hasura/go-graphql-client.queryArguments(0xc00017e1b0)
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/query.go:126 +0x350
github.com/hasura/go-graphql-client.ConstructMutation({0x6d5da0?, 0xc0001780b0?}, 0xc00017e1b0, {0x0, 0x0, 0x0})
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/query.go:80 +0x219
github.com/hasura/go-graphql-client.(*Client).buildAndRequest(0x10?, {0x7c3710, 0xc00001e5d8}, 0x34?, {0x6d5da0?, 0xc0001780b0?}, 0x40c427?, {0x0, 0x0, 0x0})
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/graphql.go:112 +0x8f
github.com/hasura/go-graphql-client.(*Client).do(0x11?, {0x7c3710?, 0xc00001e5d8?}, 0x58?, {0x6d5da0, 0xc0001780b0}, 0xc00016f960?, {0x0, 0x0, 0x0})
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/graphql.go:246 +0x65
github.com/hasura/go-graphql-client.(*Client).Mutate(...)
        /home/nico/go/pkg/mod/github.com/hasura/[email protected]/graphql.go:66
main.(*Server).update(0xc000178000)

Retrieve response body and status code

Hi,

I'm trying to fetch the HTTP request-response in order to display custom error messages when an error occurred.

I checked the code, and I found this:

https://github.com/hasura/go-graphql-client/blob/master/graphql.go#L175

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

It would be possible to have a way to retrieve the response resp so that I can get the response body and the status code?

        err := client.Mutate(ctx,  m, variables)
	if err != nil {
		 // TODO: extract the status code and response body
                // resp := client.GetResponse()) ?
	}

Thank you.

Installation instructions out-of-date / minimum go version is 1.16

Hi,

I just noticed that go-graphql-client release 0.7.1 uses io.NopCloser, which got introduced in go1.16 according to the docs. Contrary, in README.md, a minimum go version of 1.13 is stated.
Maybe bump the version number? I can create a pull request but it seems too minor of a change.

Thanks for your time!

How to refresh accessToken in onError

I am currently doing the following to refresh the accessToken whenever JWTExpired error reaches onError

client := graphql.NewSubscriptionClient(config.StrongholdGraphql).
		WithConnectionParams(map[string]interface{}{
			"headers": map[string]string{
				"Authorization": token,
			},
		}).
		WithoutLogTypes(graphql.GQLData, graphql.GQLConnectionKeepAlive).
		OnError(func(sc *graphql.SubscriptionClient, err error) error {
			logger.L.Error("Error in subscription", zap.Error(err))

			// if JWT has expired then refresh the token and start subscribing again
			if strings.Contains(err.Error(), "JWTExpired") {
				authResponse, err := fetchAuthToken()
				if err != nil {
					logger.L.Error("Error while refreshing auth token in Subscription OnError", zap.Error(err))
					return err
				}
				sc.WithConnectionParams(map[string]interface{}{
					"headers": map[string]string{
						"Authorization": authResponse.AccessToken,
					},
				})
				return sc.Run()
			}
			return sc.Run()
		})

2 Questions,

  1. Is this the right way to set the header again using WithConnectionParams()? Does sc.Run() in onError starts with the new accessToken?

  2. Getting this error

"failed to read JSON message: failed to get reader: received close frame: status = StatusCode(4400) and reason = "{\"server_error_msg\":\"4400: Connection initialization failed: Malformed Authorization header\"}""

Date/times as variable paramaters to hasura

I am currently trying to pull data through Golang against our harusa implementation. I am able to pull queries just fine until I have a date/time variable. Example:

type queryModel struct {
StatusHistory []struct {
Id hasura.BigInt graphql:"id"
Status string graphql:"status"
} graphql:"status_history(where: {ismi: {_eq: $id}, started_at: {_gte: '$ed'}})"
}

const iso8601 = "2006-01-02T15:04:05.999999-07:00"
var (
statuses = strings.Builder{}
q struct{ queryModel }
i, _ = strconv.ParseInt(dd.Id, 10, 64)
t, _ = time.Parse("01-02-2006 15:04:05", "01-23-2024 00:10:17")
tt = t.Format(iso8601)
filter = map[string]any{
"id": hasura.BigInt(i),
"ed": &tt,
}
)

	if err = d.hasura.Query(ctx, &q, filter); err != nil {
		ctx.Logger().WithError(err).Error("failed to query device data statuses")
		return err
	}

I have tried passing the "ed" variable as a string, time.Time, timestamppb, etc with no luck. I have even tried adding escaped quotes around the "$ed" notation in the query itself bound to the model. Nothing seems to work, every time it returns with an error object (though there are no errors, count == 0) and the model is not populated. However, when I remove the "ed" variable from the filters and the model query, everything works fine. So the code itself seems to be good. Also, I have run the model query directly in hasura (with the $ vars swapped out for actual values) and it all works fine. Is there a special way I should be passing in a date/time variable into the filters for the query?

Can't use pointers for GQL fragments

I'm writing a query that involves querying a type union. Here are the relevant gql typedefs:

type SellingPlanPriceAdjustment {
    adjustmentValue: SellingPlanPriceAdjustmentValue!
    orderCount: Int
}

union SellingPlanPriceAdjustmentValue = SellingPlanFixedAmountPriceAdjustment | SellingPlanFixedPriceAdjustment | SellingPlanPercentagePriceAdjustment

type SellingPlanFixedAmountPriceAdjustment {
    adjustmentAmount: MoneyV2!
}

type SellingPlanFixedPriceAdjustment {
    price: MoneyV2!
}

type SellingPlanPercentagePriceAdjustment {
    adjustmentPercentage: Int!
}

type MoneyV2 {
    amount: Decimal!
    currencyCode: CurrencyCode!
}

My query to get back the price adjustment on a selling plan is as follows:

adjustmentValue {
    ... on SellingPlanFixedAmountPriceAdjustment {
        adjustmentAmount {
            amount
            currencyCode
        }
    }
    ... on SellingPlanFixedPriceAdjustment {
        price {
            amount
            currencyCode
        }
    }
    ... on SellingPlanPercentagePriceAdjustment {
        adjustmentPercentage
    }
}

When the query gets run, I end up with a response like:

"adjustmentValue": {
  "adjustmentPercentage": 5
}

I would like for my go structs for this query to look something along the lines of:

type AdjustmentValue struct {
	PercentageAdjustment  *PercentageAdjustment  `graphql:"... on SellingPlanPercentagePriceAdjustment"`
	FixedAmountAdjustment *FixedAmountAdjustment `graphql:"... on SellingPlanFixedAmountPriceAdjustment"`
	FixedPriceAdjustment  *FixedPriceAdjustment  `graphql:"... on SellingPlanFixedPriceAdjustment"`
}

type PercentageAdjustment struct {
	AdjustmentPercentage int
}

type FixedAmountAdjustment struct {
	AdjustmentAmount Money
}

type FixedPriceAdjustment struct {
	Price Money
}

type Money struct {
	Amount       graphql.Float
	CurrencyCode string
}

Unfortunately, using pointers for the fields in the AdjustmentValue struct results in an error:

struct field for "adjustmentPercentage" doesn't exist in any of 2 places to unmarshal

I suspect that this has something to do with which fields end up getting added to the stack on d.vs in the decoder.decode() func when iterating through the frontier, but it's not totally clear to me what specifically needs to change in order to remediate this.

Here's a test case for the above:

func TestUnmarshalGraphQL_pointerForInlineFragment(t *testing.T) {
	type user struct {
		DatabaseID uint64
	}
	type actor struct {
		User *user `graphql:"... on User"`
		Login string
	}
	type query struct {
		Author actor
		Editor *actor
	}
	var got query
	err := jsonutil.UnmarshalGraphQL([]byte(`{
		"author": {
			"databaseId": 1,
			"login": "test1"
		},
		"editor": {
			"databaseId": 2,
			"login": "test2"
		}
	}`), &got)
	if err != nil {
		t.Fatal(err)
	}
	var want query
	want.Author = actor{
		User:  struct{ DatabaseID uint64 }{1},
		Login: "test1",
	}
	want.Editor = &actor{
		User:  struct{ DatabaseID uint64 }{2},
		Login: "test2",
	}

	if !reflect.DeepEqual(got, want) {
		t.Error("not equal")
	}
}

I'll add -- if this is expected behavior and not something to fix, I'd love to see the constraint reflected a bit more clearly in the gql fragment section of the docs. As-is, I'm not sure it's particularly clear that we should expect for this situation to be unsupported.

GraphQL subscription sequence is not guaranteed

HI,

thanks for a great library!

today I started testing the client with subscriptions and found out that sometimes subscription completes before emitting the last message.

After investigation I found the lines that are responsible for this behavior

go func() {

go func() {
	if err := sc.protocol.OnMessage(subContext, *sub, message); err != nil {
		sc.errorChan <- err
	}

	sc.checkSubscriptionStatuses(subContext)
}()

It seems that every message is handled in a separate goroutine, thus the order of all incoming messages (incl. errors and complete message) is not anyhow guaranteed which can be a problem in certain scenarios.

Current workaround for original issue where complete is coming before the last message is :

.OnSubscriptionComplete(func(sub graphql.Subscription) {
  handler := sub.GetHandler()
  time.Sleep(1 * time.Second) // hope for the best
  handler(nil, io.EOF)
})

that's however is quite nasty and does not anyway guarantee the message order.

How to deal with this? Does it make sense to add a configuration parameter that enforces the correct execution order even sacrificing some performance?

Remove canonical/vanity import path

Because of the comment after the package name in doc.go, the library expects to be imported as github.com/shurcooL/graphql and will not compile when imported as github.com/hasura/go-graphql-client, at least when using vendoring.

package graphql // import "github.com/shurcooL/graphql"

this is the error message I get

code in directory /workspace/src/entrypoint/vendor/github.com/hasura/go-graphql-client expects import "github.com/shurcooL/graphql"

I don't think this is the intention of this fork, so this probably should be removed.

Here is documentation about Go vanity import.

Invalid serialisation containing commas with embedded struct

I'm not a graphQL expert. Serialising this:

	type Token struct {
		IdToken      string `graphql:"id_token"`
		AccessToken  string `graphql:"access_token"`
		RefreshToken string `graphql:"refresh_token"`
		ExpiresIn    int    `graphql:"expires_in"`
	}

	var res struct {
		Token `graphql:"getAuthToken(code: $code)"`
	}

	if err := gqlClient.Query(context.Background(), &res, map[string]any{
		"code": code,
	}, graphql.OperationName("getAuthToken")); err != nil {
		return err
	}

yields

{"query":"query getAuthToken($code:String!){getAuthToken(code: $code){id_token,access_token,refresh_token,expires_in}}","variables":{"code":"EpNl5OrpZw36aKp5VSK7RUAr_FMz5UMAjhw29XFO"},"operationName":"getAuthToken"}

What strikes me strange is the commas between the field names- shouldn't that be spaces?

How to struct grapghql code

this is my grapghql structure, but I would like to know in golang

	query{
	  questions{
	    data{
	      	attributes{
	          title
	          description
	          question_id
	        }
	    }

  }
}

I wrote code for client to get data from graphql, Like this but it's not working.

var que struct {
		Questions struct {
			Data struct {
				Attributes []struct {
					Question_id graphql.String
					Title       graphql.String
					Description graphql.String
				}
			}
		}
	}

showing this Error
slice doesn't exist in any of 1 places to unmarshalError comes in exit status 1

in browser showing data Like this
{ "data": { "questions": { "data": [ { "attributes": { "title": "I got issue with keyword error", "description": "I have issue with keyerror in python", "question_id": null } }, { "attributes": { "title": "memory error", "description": "while runnig code i got this issue", "question_id": null } } ] } } }

ExecRaw method

Hey Hasura community,

Is there any reason there is no ExecRaw() method to get the raw response?

mutation in golang client not working

Can you Please help to find solution, For Mutation
var m struct { CreateQuestion struct { Data struct { Id graphql.ID Attributes struct { Question_id graphql.String Title graphql.String Description graphql.String } } } graphql:"createQuestion(data: $data)"}
Defined Variables Like this
type user_insert_input map[string]interface{} variables := map[string]interface{}{ "data": []user_insert_input{ "question_id": "2451", "title": "python struct", "description": "brief description of python syntax error", }, } err := cli.Mutate(context.Background(), &m, variables)
Error was Look like
cannot convert "question_id" (untyped string constant) to int ./client.go:69:19: cannot use "2451" (untyped string constant) as user_insert_input value in array or slice literal

field 'update_schema_event' not found in type: 'query_root',

am doing this query and am always getting
error Message: field 'update_schema_event' not found in type: 'query_root', Locations: [], Extensions: map[code:validation-failed path:$.selectionSet.update_schema_event]
when i do it in hasura console it works

code

func UpdateSchemaResult(id int, result string) error {
	client := startgraphqlconnection()

	var mutation struct {
		update_schema_event struct {
			Affected_Rows int `json:"affected_rows"`
		} `graphql:"update_schema_event(where: { id: { _eq: $id }}, _set: { result_id: $result })"`
	}
	variables := map[string]interface{}{
		"id":     id,
		"result": result,
	}

	err := client.Query(context.Background(), &mutation, variables)
	if err != nil {
		fmt.Println("error", err)
		return err
	}
	return nil
}

i also tried with this method but the same result

	var mutation struct {
		Updateschemaevent struct {
			AffectedRows int `graphql:"affected_rows"`
		} `graphql:"update_schema_event(where: $where, _set: $set)"`
	}
	variables := map[string]interface{}{
		"where": schema_event_bool_exp{
			"id": map[string]interface{}{
				"_eq": id,
			},
		},
		"set": schema_event_set_input{
			"result_id": result,
		},
	}

thanks for help

ExecRaw vs Mutate/MutateRaw

Hi,

I am having trouble understanding why for my case, I can only use ExecRaw instead of Mutate/MutateRaw. Hopefully someone can help me understand this.

I have a mutation like this on my server (not Hasura):

report(inputs: [JSON]!): [String!]!

On the client side, I have something like this:

var inputs = []health.DeviceInfo{} //imagine i already had some data in here

type NetworkInfo struct {
	Latency   string `json:"latency"`
	IPAddress string `json:"ipAddress"`
}

type DeviceInfo struct {
	Network  NetworkInfo `json:"network"`
	RecordId string      `json:"recordId"`
}

Solution 1: Using Mutate/MutateRaw

var mutation struct {
        Result []string `graphql:"report(inputs: $input)"`
}

var variables := map[string]interface{}{
        "inputs":  inputs,
}

err := client.Mutate(context.Background(), &mutation, variables)

This solution did not work for me. The server will throw an error Unknown type DeviceType. If I attempted to marshal that input and convert to json string then the server will throw error Expect [JSONObject] not [String]
But if I implemented as Solution 2:

var mutation = `mutation ($input: [JSON]!) {result: report(inputs: $input)}`
result, err := client.ExecRaw(context.Background(), mutation, variables)

This solution works for me. The server did not throw any error and response properly.

I think it has something to do with using the struct vs string for the mutation but i'm not sure. If someone knows why, please help my understand more. I am still very new to Go. Thank you!

Run() semantics- how to check for error?

I'm trying to figure out if the client actually successfully started:

client := graphql.NewSubscriptionClient(tibber.SubscriptionURI).
	WithConnectionParams(map[string]any{
		"token": cc.Token,
	}).
	WithLog(t.log.TRACE.Println)

// run the client
errC := make(chan error)

go func() {
	errC <- client.Run()
}()

err := <-errC

Unfortunately this will deadlock sind Run() will only return if it errors. It would be nice if we had a means of determining if the client started successfully, too.

SubscriptionClient OnDisconnected() regression in v0.9.1

I have a subscription resolver that closes the connection server-side and when I upgraded to go-graphql-client v0.9.1 I noticed that the SubscriptionClient stopped calling the OnDisconnected() method on server-close. I'm attaching a test file that demonstrates this behavior (see below).

This is the log output in v0.9.0 ("OnDisconnected" present):

2023/03/07 12:43:10 {"type":"connection_init","payload":{"headers":{"foo":"bar"}}} client
2023/03/07 12:43:10 {"type":"connection_ack"} server
2023/03/07 12:43:10 {"id":"88b45ff2-b349-420c-866e-1a23d694649b","type":"start","payload":{"query":"subscription{helloGoodbye}"}} client
2023/03/07 12:43:10 OnConnected
2023/03/07 12:43:10 {"id":"88b45ff2-b349-420c-866e-1a23d694649b","type":"data","payload":{"data":{"helloGoodbye":"hello"}}} server
2023/03/07 12:43:10 {"id":"88b45ff2-b349-420c-866e-1a23d694649b","type":"data","payload":{"data":{"helloGoodbye":"goodbye"}}} server
2023/03/07 12:43:10 {"id":"88b45ff2-b349-420c-866e-1a23d694649b","type":"complete"} server
2023/03/07 12:43:10 {"id":"88b45ff2-b349-420c-866e-1a23d694649b","type":"stop"} client
2023/03/07 12:43:10 no running subscription. exiting... client
2023/03/07 12:43:11 http: Server closed
2023/03/07 12:43:11 OnDisconnected

And this is the log output in v0.9.1 ("OnDisconnected" missing):

2023/03/07 12:36:55 {"type":"connection_init","payload":{"headers":{"foo":"bar"}}} client
2023/03/07 12:36:55 {"type":"connection_ack"} server
2023/03/07 12:36:55 {"id":"33a56134-b9d4-4d1c-ac35-dc05fdbc4090","type":"start","payload":{"query":"subscription{helloGoodbye}"}} client
2023/03/07 12:36:55 OnConnected
2023/03/07 12:36:55 {"id":"33a56134-b9d4-4d1c-ac35-dc05fdbc4090","type":"data","payload":{"data":{"helloGoodbye":"hello"}}} server
2023/03/07 12:36:55 {"id":"33a56134-b9d4-4d1c-ac35-dc05fdbc4090","type":"data","payload":{"data":{"helloGoodbye":"goodbye"}}} server
2023/03/07 12:36:55 {"id":"33a56134-b9d4-4d1c-ac35-dc05fdbc4090","type":"complete"} server
2023/03/07 12:36:55 {"id":"33a56134-b9d4-4d1c-ac35-dc05fdbc4090","type":"stop"} client
2023/03/07 12:36:55 no running subscription. exiting... client
2023/03/07 12:36:56 http: Server closed

Is this behavior expected?

Here is the test file (will run in root directory of go-graphql-client):

package graphql

import (
	"context"
	"log"
	"net/http"
	"testing"
	"time"

	"github.com/graph-gophers/graphql-go"
	"github.com/graph-gophers/graphql-go/relay"
	"github.com/graph-gophers/graphql-transport-ws/graphqlws"
)

const sc_Schema = `
schema {
	subscription: Subscription
	query: Query
}

type Query {
}

type Subscription {
	helloGoodbye(): String!
}
`

type sc_Resolver struct{}

func (r *sc_Resolver) HelloGoodbye(ctx context.Context) <-chan string {
	c := make(chan string, 10)
	go func() {
		c <- "hello"
		c <- "goodbye"
		close(c)
	}()
	return c
}

func sc_setupServer() *http.Server {
	// init schema
	s, err := graphql.ParseSchema(sc_Schema, &sc_Resolver{})
	if err != nil {
		panic(err)
	}

	// graphql handler
	mux := http.NewServeMux()
	graphQLHandler := graphqlws.NewHandlerFunc(s, &relay.Handler{Schema: s})
	mux.HandleFunc("/graphql", graphQLHandler)
	server := &http.Server{Addr: ":8081", Handler: mux}

	return server
}

func TestSubscriptionClose(t *testing.T) {
	// init server
	server := sc_setupServer()
	go func() {
		if err := server.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	// init client
	client := NewSubscriptionClient("http://localhost:8081/graphql").
		WithConnectionParams(map[string]interface{}{
			"headers": map[string]string{
				"foo": "bar",
			},
		}).
		WithLog(log.Println).
		OnConnected(func() {
			log.Println("OnConnected")
		}).
		OnDisconnected(func() {
			log.Println("OnDisconnected")
		})

	// set up subscription
	var subscription struct {
		HelloGoodbye string
	}
	_, err := client.Subscribe(&subscription, nil, func(data []byte, e error) error {
		if e != nil {
			t.Fatal(e)
		}
		return e
	})
	if err != nil {
		t.Fatal(err)
	}

	// run client
	go func() {
		client.Run()
	}()
	defer client.Close()

	// stop test after 1 sec
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer server.Shutdown(ctx)
	defer cancel()
	<-ctx.Done()
}

Debugging queries

Hi, I want to subscribe to an API, but I get an error:

failed to read JSON message: failed to get reader: received close frame: status = StatusCode(4500) and reason = "Syntax Error: Expected Name, found \"!\"."

But I don't know where that exclamation mark comes from. How can I debug this? Is there a way to print the query out before subscribing?

Get list/map of subscriptions

I'm currently utilizing the GetSubscription(id string) method in the subscription client, which allows to retrieve a subscription by its ID. However, I'd like to propose a new feature that could enhance the functionality of the subscription client.

Could your consider adding a new method that returns a list or map of all subscriptions that have been registered by a client?

Use case:
The motivation behind this request is to improve error handling. When an error occurs, the OnError callback is triggered. I want to engage all subscription handlers and pass along the error. As it stands, I need to manually track all subscriptions because the subscription client doesn't reveal the underlying subscriptions map.

This proposed feature could streamline the process and enhance the efficiency of error handling.

UUID Error

`
func InsertUserPassword(user_id, password string) (string,error){
client := config.GraphqlClient()
parsedUserId, _ := uuid.Parse(user_id)

variables := map[string]interface{}{     
	"user_id": parsedUserId.String(),     
	"password": password,       
}

fmt.Println("Type Of UUID",reflect.TypeOf(variables["user_id"]))
fmt.Println("Type Of String",reflect.TypeOf(variables["password"]))
var response mutation         

err := client.Mutate(context.Background(),&response, variables)
if err != nil {   
	fmt.Println("An error occurred:", err)   
	return "", errors.New("error fetching user data")
}
responseJSON, _ := json.Marshal(response)           
fmt.Println(string(responseJSON))         
return "",nil

}
`

but it throws
Message: variable 'user_id' is declared as 'String!', but used where 'uuid!' is expected, Locations: [], Extensions: map[code:validation-failed path:$.selectionSet.update_authentications_by_pk.args.pk_columns.user_id]
I tried by removing String()

Smallint type problem

Hello,

Thanks for great work, i appreciate it. Library works great with Hasura but i am encountered a bug.

When mutating, i must send smallint as type but when i create smallint as type on Golang it still returns Int!

This typecasting works with strings without problems but on int values i encounter this error;

Message: variable 'user_defined' is declared as 'Int', but used where 'smallint' is expected, Locations: [], Extensions: map[code:validation-failed path:$.selectionSet.....]

image

Thanks.

jsonb type declaration

{
"uuid": "efgh-5678-abcd-1234",
"network": "avalanche",
"address": "0x9876543210987654321098765432109876543210",
"rulechain": false,
"topics": [
"0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"0x5e4f3d2c1b0a99887766554433221100"
]
}

i have data to be send like this
func QuerySchema(sc Variables_schema) (int, error) {
/*
query MyQuery($ca: String, $nt: String , $tp: jsonb) {
schema_event(where: {topics: {_contained_in: $tp}, contractaddress: {_eq: $ca}, network: {_eq: $nt}}) {
id
}
}
*/

jsondata, err := json.Marshal(sc.Topics)
if err != nil {
	return 0, err
}

client := startgraphqlconnection()
var query struct {
	SchemaEvent []struct {
		ID int `json:"id"`
	} `graphql:"schema_event(where: {contractaddress: {_eq: $ca}, network: {_eq: $nt}})"`
}
variables := map[string]interface{}{
	"ca": sc.ContractAddress,
	"nt": sc.Network,
	"tp": string(jsondata),
}

fmt.Print(variables)
if err := client.Query(context.Background(), &query, variables); err != nil {
	fmt.Print(err)
	return 0, err
} else {
	if len(query.SchemaEvent) == 0 {
		// not found logic
		return 0, nil
	} else {
		// found logic
		fmt.Print(query)
		return query.SchemaEvent[0].ID, nil
	}
}

}

am always getting this return
map[ca:0x9876543210987654321098765432109876543210 nt:avalanche tp:["0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6","0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d","0x5e4f3d2c1b0a99887766554433221100"]]Message: unexpected variables in variableValues: tp, Locations: [], Extensions: map[code:validation-failed path:$]inside the qyeryschema[GIN] 2023/11/21 - 13:41:21 | 500 | 558.581868ms | ::1 | POST "/api/v1/subscribe"

[BUG] runtime: goroutine stack exceeds 1000000000-byte limit

This could be a https://github.com/shurcooL/graphql issue but its a big blocker for me.

I can't do any mutation.

Example:

	var m struct {
		AddPerson struct {
			Input  []*model.AddPersonInput
			Upsert *bool
		} `graphql:"addPerson(input: $input, upsert: $upsert)"`
	}

	variables := map[string]interface{}{
		"input":  input,
		"upsert": &upsert,
	}

	err := r.client.Mutate(context.Background(), &m, variables)
	if err != nil {
		return nil, err
	}
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc020700388 stack=[0xc020700000, 0xc040700000]
fatal error: stack overflow

security issue with github.com/gin-gonic/gin

github's dependabot flagged versions < 1.7.0 of github.com/gin-gonic/gin with the following issue

This affects all versions of package github.com/gin-gonic/gin under 1.7.0. When gin is exposed directly to the internet, a client's IP can be spoofed by setting the X-Forwarded-For header.

Update the updated_at value using the Mutate method

          I'm trying to update the updated_at value using the Mutate method. 

and the solution provided did not work for me, and it throws the following exception:

   `parsing UTCTime failed, expected String, but encountered Object, Locations: []`

The reason behind it is the value of timestamptz(time.Now()) is equivalent to time.Time{} object, and hasura is waiting for String to be parsed. knowing that sending a string will throw the following error:

variable 'time' is declared as 'String!', but used where 'timestamptz' is expected, Locations: []

Building the query inside the buildAndRequest method do the job, but cannot handle having the 'timestamptz' to be sent as String.

If there are no solution, a possible solution might be by editing the queryArguments in ConstructMutation method, to make optional parsing of the arguments manually by the developer(similar to OptionType ..eg.. OperationName implementation by providing the Arguments' names as another Option), rather than only parsing it using only the implemented code.

Originally posted by @NajyFannoun in #51 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.