diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1732c8..1c8aa37 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,13 +14,15 @@ jobs: test-go: name: Run Go lint and unit tests runs-on: ubuntu-20.04 + permissions: + pull-requests: write steps: - name: Checkout - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: - go-version: "1.16.4" - - uses: actions/cache@v2 + go-version: "1.20" + - uses: actions/cache@v3 with: path: | ~/go/pkg/mod @@ -29,7 +31,9 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install dependencies - run: go get -t -v ./... + run: | + go get -t -v ./... + go install ./... - name: Format run: diff -u <(echo -n) <(gofmt -d -s .) - name: Vet @@ -43,6 +47,7 @@ jobs: - name: Go coverage format run: | go get github.com/boumenot/gocover-cobertura + go install github.com/boumenot/gocover-cobertura gocover-cobertura < coverage.out > coverage.xml - name: Code Coverage Summary Report uses: irongut/CodeCoverageSummary@v1.3.0 @@ -58,9 +63,8 @@ jobs: thresholds: "60 80" - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 - if: github.event_name == 'pull_request' + if: ${{ github.event_name == 'pull_request_target' }} with: - recreate: true path: code-coverage-results.md - name: Dump docker logs on failure if: failure() diff --git a/README.md b/README.md index ed9086c..7deeb09 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,12 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu - [Directories](#directories) - [References](#references) - [License](#license) - + ## Installation -`go-graphql-client` requires Go version 1.16 or later. For older Go versions, downgrade the library version below v0.7.1. +`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. ```bash go get -u github.com/hasura/go-graphql-client @@ -202,7 +204,7 @@ var mutation struct { CreateDimensions struct { ID string `graphql:"id"` } `graphql:"create_dimensions(ship_dimensions: $ship_dimensions)"` -} +} variables := map[string]interface{}{ "ship_dimensions": myDimensions, @@ -234,7 +236,7 @@ struct { // login // createdAt // databaseId -// } +// } // } struct { @@ -559,7 +561,7 @@ client := graphql.NewSubscriptionClient("wss://example.com/graphql"). "authentication": "...", }, }). - // or lazy parameters with function + // or lazy parameters with function WithConnectionParamsFn(func () map[string]interface{} { return map[string]interface{} { "headers": map[string]string{ @@ -575,7 +577,7 @@ client := graphql.NewSubscriptionClient("wss://example.com/graphql"). ```Go client. // write timeout of websocket client - WithTimeout(time.Minute). + 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 @@ -587,7 +589,7 @@ client. // 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 + // the input parameter can be number string or range, e.g 4000-5000 WithRetryStatusCodes("4000", "4000-4050") ``` @@ -796,7 +798,7 @@ var res struct { Somethings []Something `json:"something"` } -raw, err := client.ExecRaw(ctx, query, map[string]any{}) +raw, err := client.ExecRaw(ctx, query, map[string]any{}) if err != nil { panic(err) } @@ -942,11 +944,11 @@ func ConstructQuery(v interface{}, variables map[string]interface{}, options ... 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, error) +func ConstructSubscription(v interface{}, variables map[string]interface{}, options ...Option) (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 +func UnmarshalGraphQL(data []byte, v interface{}) error ``` Directories diff --git a/go.mod b/go.mod index 9f9bde2..16b0aa3 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,19 @@ module github.com/hasura/go-graphql-client -go 1.16 +go 1.20 require ( github.com/google/uuid v1.3.0 github.com/graph-gophers/graphql-go v1.5.0 github.com/graph-gophers/graphql-transport-ws v0.0.2 + nhooyr.io/websocket v1.8.7 +) + +require ( + github.com/gorilla/websocket v1.5.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - nhooyr.io/websocket v1.8.7 ) replace github.com/gin-gonic/gin v1.6.3 => github.com/gin-gonic/gin v1.7.7 diff --git a/go.sum b/go.sum index bdf7594..d9d7fcd 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo= @@ -40,6 +42,8 @@ github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGn github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= diff --git a/graphql.go b/graphql.go index 1e07c09..7fe1ed7 100644 --- a/graphql.go +++ b/graphql.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "strings" @@ -105,26 +104,32 @@ func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) { var query string var err error + var optionOutput *constructOptionsOutput switch op { case queryOperation: - query, err = ConstructQuery(v, variables, options...) + query, optionOutput, err = constructQuery(v, variables, options...) case mutationOperation: - query, err = ConstructMutation(v, variables, options...) + query, optionOutput, err = constructMutation(v, variables, options...) } if err != nil { return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)} } - return c.request(ctx, query, variables, options...) + return c.request(ctx, query, variables, optionOutput) } // Request the common method that send graphql request -func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) { +func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options *constructOptionsOutput) ([]byte, *http.Response, io.Reader, Errors) { in := GraphQLRequestPayload{ Query: query, Variables: variables, } + + if options != nil { + in.OperationName = options.operationName + } + var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(in) if err != nil { @@ -173,7 +178,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string } if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) err := newError(ErrRequestError, fmt.Errorf("%v; body: %q", resp.Status, body)) if c.debug { @@ -190,7 +195,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string // copy the response reader for debugging var respReader *bytes.Reader if c.debug { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, nil, Errors{newError(ErrJsonDecode, err)} } @@ -250,14 +255,24 @@ func (c *Client) do(ctx context.Context, op operationType, v interface{}, variab // Executes a pre-built query and unmarshals the response into v. Unlike the Query method you have to specify in the query the // fields that you want to receive as they are not inferred from v. This method is useful if you need to build the query dynamically. func (c *Client) Exec(ctx context.Context, query string, v interface{}, variables map[string]interface{}, options ...Option) error { - data, resp, respBuf, errs := c.request(ctx, query, variables, options...) + optionsOutput, err := constructOptions(options) + if err != nil { + return err + } + + data, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput) return c.processResponse(v, data, resp, respBuf, errs) } // Executes a pre-built query and returns the raw json message. Unlike the Query method you have to specify in the query the // fields that you want to receive as they are not inferred from the interface. This method is useful if you need to build the query dynamically. func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, error) { - data, _, _, errs := c.request(ctx, query, variables, options...) + optionsOutput, err := constructOptions(options) + if err != nil { + return nil, err + } + + data, _, _, errs := c.request(ctx, query, variables, optionsOutput) if len(errs) > 0 { return data, errs } @@ -356,7 +371,7 @@ func newError(code string, err error) Error { func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error { internal := e.getInternalExtension() - bodyBytes, err := ioutil.ReadAll(bodyReader) + bodyBytes, err := io.ReadAll(bodyReader) if err != nil { internal["error"] = err } else { @@ -375,7 +390,7 @@ func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error { func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error { internal := e.getInternalExtension() - bodyBytes, err := ioutil.ReadAll(bodyReader) + bodyBytes, err := io.ReadAll(bodyReader) if err != nil { internal["error"] = err } else { diff --git a/graphql_test.go b/graphql_test.go index 05e7144..dfc7c1c 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "io" - "io/ioutil" "net/http" "net/http/httptest" "testing" @@ -427,7 +426,7 @@ func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) } func mustRead(r io.Reader) string { - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { panic(err) } diff --git a/pkg/jsonutil/graphql.go b/pkg/jsonutil/graphql.go index 1b9dce1..0845a63 100644 --- a/pkg/jsonutil/graphql.go +++ b/pkg/jsonutil/graphql.go @@ -165,13 +165,15 @@ func (d *decoder) decode() error { if v.Kind() == reflect.Slice { // we want to append the template item copy // so that all the inner structure gets preserved - copied, err := copyTemplate(v.Index(0)) - if err != nil { - return fmt.Errorf("failed to copy template: %w", err) + if v.Len() != 0 { + copied, err := copyTemplate(v.Index(0)) + if err != nil { + return fmt.Errorf("failed to copy template: %w", err) + } + v.Set(reflect.Append(v, copied)) // v = append(v, T). + f = v.Index(v.Len() - 1) + someSliceExist = true } - v.Set(reflect.Append(v, copied)) // v = append(v, T). - f = v.Index(v.Len() - 1) - someSliceExist = true } d.vs[i] = append(d.vs[i], f) } diff --git a/query.go b/query.go index 65748b0..1ee9d7c 100644 --- a/query.go +++ b/query.go @@ -43,48 +43,68 @@ func constructOptions(options []Option) (*constructOptionsOutput, error) { return output, nil } -// ConstructQuery build GraphQL query string from struct and variables -func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error) { +func constructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, *constructOptionsOutput, error) { query, err := query(v) if err != nil { - return "", err + return "", nil, err } optionsOutput, err := constructOptions(options) if err != nil { - return "", err + return "", nil, err } if len(variables) > 0 { - return fmt.Sprintf("query %s(%s)%s%s", optionsOutput.operationName, queryArguments(variables), optionsOutput.OperationDirectivesString(), query), nil + return fmt.Sprintf("query %s(%s)%s%s", optionsOutput.operationName, queryArguments(variables), optionsOutput.OperationDirectivesString(), query), optionsOutput, nil } if optionsOutput.operationName == "" && len(optionsOutput.operationDirectives) == 0 { - return query, nil + return query, optionsOutput, nil } - return fmt.Sprintf("query %s%s%s", optionsOutput.operationName, optionsOutput.OperationDirectivesString(), query), nil + return fmt.Sprintf("query %s%s%s", optionsOutput.operationName, optionsOutput.OperationDirectivesString(), query), optionsOutput, nil } -// ConstructQuery build GraphQL mutation string from struct and variables -func ConstructMutation(v interface{}, variables map[string]interface{}, options ...Option) (string, error) { - query, err := query(v) +// ConstructQuery build GraphQL query string from struct and variables +func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error) { + query, _, err := constructQuery(v, variables, options...) if err != nil { return "", err } + + return query, err +} + +func constructMutation(v interface{}, variables map[string]interface{}, options ...Option) (string, *constructOptionsOutput, error) { + query, err := query(v) + if err != nil { + return "", nil, err + } + optionsOutput, err := constructOptions(options) if err != nil { - return "", err + return "", nil, err } + if len(variables) > 0 { - return fmt.Sprintf("mutation %s(%s)%s%s", optionsOutput.operationName, queryArguments(variables), optionsOutput.OperationDirectivesString(), query), nil + return fmt.Sprintf("mutation %s(%s)%s%s", optionsOutput.operationName, queryArguments(variables), optionsOutput.OperationDirectivesString(), query), optionsOutput, nil } if optionsOutput.operationName == "" && len(optionsOutput.operationDirectives) == 0 { - return "mutation" + query, nil + return "mutation" + query, optionsOutput, nil + } + + return fmt.Sprintf("mutation %s%s%s", optionsOutput.operationName, optionsOutput.OperationDirectivesString(), query), optionsOutput, nil +} + +// ConstructMutation build GraphQL mutation string from struct and variables +func ConstructMutation(v interface{}, variables map[string]interface{}, options ...Option) (string, error) { + query, _, err := constructMutation(v, variables, options...) + if err != nil { + return "", err } - return fmt.Sprintf("mutation %s%s%s", optionsOutput.operationName, optionsOutput.OperationDirectivesString(), query), nil + return query, err } // ConstructSubscription build GraphQL subscription string from struct and variables