diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bec20ef4..33505a3c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,7 @@ When releasing a new version: ### New features: +- The new `graphql.WithOperationNameParam` allows clients to be created that include `operationName` as a query parameter. - The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details. - For schemas with enum values that differ only in casing, it's now possible to disable smart-casing in genqlient.yaml; see the [documentation](genqlient.yaml) for `casing` for details. diff --git a/docs/FAQ.md b/docs/FAQ.md index 35b084c1..0f267fb7 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -36,6 +36,19 @@ The URL requested will be: The client does not support mutations, and will return an error if passed a request that attempts one. +### … include operationName as a query parameter? + +`graphql.NewClient` support the option `graphql.WithOperationNameParam`, which will add `?operationName=...` to the request's query parameters. + +```go +ctx := context.Background() +client := graphql.NewClient("https://api.github.com/graphql", http.DefaultClient, graphql.WithOperationNameParam) +resp, err := getUser(ctx, client, "benjaminjkraft") +fmt.Println(resp.User.Name, err) +``` + + + ### … use an API that requires authentication? When you call `graphql.NewClient`, pass in an HTTP client that adds whatever authentication headers you need (typically by wrapping the client's `Transport`). For example: diff --git a/graphql/client.go b/graphql/client.go index 469e3c04..7abe6e81 100644 --- a/graphql/client.go +++ b/graphql/client.go @@ -41,9 +41,15 @@ type Client interface { } type client struct { - httpClient Doer - endpoint string - method string + httpClient Doer + endpoint string + method string + withOperationNameParam bool +} + +// WithOperationNameParam allows operationName to be included as a query parameter. +func WithOperationNameParam(c *client) { + c.withOperationNameParam = true } // NewClient returns a [Client] which makes requests to the given endpoint, @@ -58,8 +64,8 @@ type client struct { // example. // // [example/main.go]: https://github.com/Khan/genqlient/blob/main/example/main.go#L12-L20 -func NewClient(endpoint string, httpClient Doer) Client { - return newClient(endpoint, httpClient, http.MethodPost) +func NewClient(endpoint string, httpClient Doer, options ...func(*client)) Client { + return newClient(endpoint, httpClient, http.MethodPost, options...) } // NewClientUsingGet returns a [Client] which makes GET requests to the given @@ -83,11 +89,19 @@ func NewClientUsingGet(endpoint string, httpClient Doer) Client { return newClient(endpoint, httpClient, http.MethodGet) } -func newClient(endpoint string, httpClient Doer, method string) Client { +func newClient(endpoint string, httpClient Doer, method string, options ...func(*client)) Client { if httpClient == nil || httpClient == (*http.Client)(nil) { httpClient = http.DefaultClient } - return &client{httpClient, endpoint, method} + c := &client{ + httpClient: httpClient, + endpoint: endpoint, + method: method, + } + for _, opt := range options { + opt(c) + } + return c } // Doer encapsulates the methods from [*http.Client] needed by [Client]. @@ -178,6 +192,23 @@ func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response) } func (c *client) createPostRequest(req *Request) (*http.Request, error) { + parsedURL, err := url.Parse(c.endpoint) + if err != nil { + return nil, err + } + + queryParams := parsedURL.Query() + queryUpdated := false + + if c.withOperationNameParam && req.OpName != "" { + queryParams.Set("operationName", req.OpName) + queryUpdated = true + } + + if queryUpdated { + parsedURL.RawQuery = queryParams.Encode() + } + body, err := json.Marshal(req) if err != nil { return nil, err @@ -185,7 +216,7 @@ func (c *client) createPostRequest(req *Request) (*http.Request, error) { httpReq, err := http.NewRequest( c.method, - c.endpoint, + parsedURL.String(), bytes.NewReader(body)) if err != nil { return nil, err diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index 0e056fab..a99ec371 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -107,6 +107,7 @@ func TestVariables(t *testing.T) { // worry about it. clients := []graphql.Client{ graphql.NewClient(server.URL, http.DefaultClient), + graphql.NewClient(server.URL, http.DefaultClient, graphql.WithOperationNameParam), graphql.NewClientUsingGet(server.URL, http.DefaultClient), } diff --git a/internal/integration/roundtrip.go b/internal/integration/roundtrip.go index 1be9996a..831978ef 100644 --- a/internal/integration/roundtrip.go +++ b/internal/integration/roundtrip.go @@ -104,7 +104,11 @@ func (c *roundtripClient) MakeRequest(ctx context.Context, req *graphql.Request, } func newRoundtripClients(t *testing.T, endpoint string) []graphql.Client { - return []graphql.Client{newRoundtripClient(t, endpoint), newRoundtripGetClient(t, endpoint)} + return []graphql.Client{ + newRoundtripClient(t, endpoint), + newRoundtripClientWithOptions(t, endpoint), + newRoundtripGetClient(t, endpoint), + } } func newRoundtripClient(t *testing.T, endpoint string) graphql.Client { @@ -117,6 +121,16 @@ func newRoundtripClient(t *testing.T, endpoint string) graphql.Client { } } +func newRoundtripClientWithOptions(t *testing.T, endpoint string) graphql.Client { + transport := &lastResponseTransport{wrapped: http.DefaultTransport} + httpClient := &http.Client{Transport: transport} + return &roundtripClient{ + wrapped: graphql.NewClient(endpoint, httpClient, graphql.WithOperationNameParam), + transport: transport, + t: t, + } +} + func newRoundtripGetClient(t *testing.T, endpoint string) graphql.Client { transport := &lastResponseTransport{wrapped: http.DefaultTransport} httpClient := &http.Client{Transport: transport}