github.com/opiuman/genqlient@v1.0.0/graphql/client.go (about) 1 package graphql 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "strings" 13 14 "github.com/vektah/gqlparser/v2/gqlerror" 15 ) 16 17 // RequestOption is a function that can be passed to MakeRequest to modify the http request behavior. 18 type RequestOption func(req *http.Request) 19 20 // Client is the interface that the generated code calls into to actually make 21 // requests. 22 type Client interface { 23 // MakeRequest must make a request to the client's GraphQL API. 24 // 25 // ctx is the context that should be used to make this request. If context 26 // is disabled in the genqlient settings, this will be set to 27 // context.Background(). 28 // 29 // req contains the data to be sent to the GraphQL server. Typically GraphQL 30 // APIs will expect it to simply be marshalled as JSON, but MakeRequest may 31 // customize this. 32 // 33 // resp is the Response object into which the server's response will be 34 // unmarshalled. Typically GraphQL APIs will return JSON which can be 35 // unmarshalled directly into resp, but MakeRequest can customize it. 36 // If the response contains an error, this must also be returned by 37 // MakeRequest. The field resp.Data will be prepopulated with a pointer 38 // to an empty struct of the correct generated type (e.g. MyQueryResponse). 39 MakeRequest( 40 ctx context.Context, 41 req *Request, 42 resp *Response, 43 opts ...RequestOption, 44 ) error 45 } 46 47 type client struct { 48 httpClient Doer 49 endpoint string 50 method string 51 } 52 53 // NewClient returns a Client which makes requests to the given endpoint, 54 // suitable for most users. 55 // 56 // The client makes POST requests to the given GraphQL endpoint using standard 57 // GraphQL HTTP-over-JSON transport. It will use the given http client, or 58 // http.DefaultClient if a nil client is passed. 59 // 60 // The typical method of adding authentication headers is to wrap the client's 61 // Transport to add those headers. See example/caller.go for an example. 62 func NewClient(endpoint string, httpClient Doer) Client { 63 return newClient(endpoint, httpClient, http.MethodPost) 64 } 65 66 // NewClientUsingGet returns a Client which makes requests to the given endpoint, 67 // suitable for most users. 68 // 69 // The client makes GET requests to the given GraphQL endpoint using a GET query, 70 // with the query, operation name and variables encoded as URL parameters. 71 // It will use the given http client, or http.DefaultClient if a nil client is passed. 72 // 73 // The client does not support mutations, and will return an error if passed a request 74 // that attempts one. 75 // 76 // The typical method of adding authentication headers is to wrap the client's 77 // Transport to add those headers. See example/caller.go for an example. 78 func NewClientUsingGet(endpoint string, httpClient Doer) Client { 79 return newClient(endpoint, httpClient, http.MethodGet) 80 } 81 82 func newClient(endpoint string, httpClient Doer, method string) Client { 83 if httpClient == nil || httpClient == (*http.Client)(nil) { 84 httpClient = http.DefaultClient 85 } 86 return &client{httpClient, endpoint, method} 87 } 88 89 // Doer encapsulates the methods from *http.Client needed by Client. 90 // The methods should have behavior to match that of *http.Client 91 // (or mocks for the same). 92 type Doer interface { 93 Do(*http.Request) (*http.Response, error) 94 } 95 96 // Request contains all the values required to build queries executed by 97 // the graphql.Client. 98 // 99 // Typically, GraphQL APIs will accept a JSON payload of the form 100 // {"query": "query myQuery { ... }", "variables": {...}}` 101 // and Request marshals to this format. However, MakeRequest may 102 // marshal the data in some other way desired by the backend. 103 type Request struct { 104 // The literal string representing the GraphQL query, e.g. 105 // `query myQuery { myField }`. 106 Query string `json:"query"` 107 // A JSON-marshalable value containing the variables to be sent 108 // along with the query, or nil if there are none. 109 Variables interface{} `json:"variables,omitempty"` 110 // The GraphQL operation name. The server typically doesn't 111 // require this unless there are multiple queries in the 112 // document, but genqlient sets it unconditionally anyway. 113 OpName string `json:"operationName"` 114 } 115 116 // Response that contains data returned by the GraphQL API. 117 // 118 // Typically, GraphQL APIs will return a JSON payload of the form 119 // {"data": {...}, "errors": {...}} 120 // It may additionally contain a key named "extensions", that 121 // might hold GraphQL protocol extensions. Extensions and Errors 122 // are optional, depending on the values returned by the server. 123 type Response struct { 124 Data interface{} `json:"data"` 125 Extensions map[string]interface{} `json:"extensions,omitempty"` 126 Errors gqlerror.List `json:"errors,omitempty"` 127 } 128 129 func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response, opts ...RequestOption) error { 130 var httpReq *http.Request 131 var err error 132 if c.method == http.MethodGet { 133 httpReq, err = c.createGetRequest(req) 134 } else { 135 httpReq, err = c.createPostRequest(req) 136 } 137 138 if err != nil { 139 return err 140 } 141 httpReq.Header.Set("Content-Type", "application/json") 142 for _, opt := range opts { 143 opt(httpReq) 144 } 145 146 if ctx != nil { 147 httpReq = httpReq.WithContext(ctx) 148 } 149 150 httpResp, err := c.httpClient.Do(httpReq) 151 if err != nil { 152 return err 153 } 154 defer httpResp.Body.Close() 155 156 if httpResp.StatusCode != http.StatusOK { 157 var respBody []byte 158 respBody, err = io.ReadAll(httpResp.Body) 159 if err != nil { 160 respBody = []byte(fmt.Sprintf("<unreadable: %v>", err)) 161 } 162 return fmt.Errorf("returned error %v: %s", httpResp.Status, respBody) 163 } 164 165 err = json.NewDecoder(httpResp.Body).Decode(resp) 166 if err != nil { 167 return err 168 } 169 if len(resp.Errors) > 0 { 170 return resp.Errors 171 } 172 return nil 173 } 174 175 func (c *client) createPostRequest(req *Request) (*http.Request, error) { 176 body, err := json.Marshal(req) 177 if err != nil { 178 return nil, err 179 } 180 181 httpReq, err := http.NewRequest( 182 c.method, 183 c.endpoint, 184 bytes.NewReader(body)) 185 if err != nil { 186 return nil, err 187 } 188 189 return httpReq, nil 190 } 191 192 func (c *client) createGetRequest(req *Request) (*http.Request, error) { 193 parsedURL, err := url.Parse(c.endpoint) 194 if err != nil { 195 return nil, err 196 } 197 198 queryParams := parsedURL.Query() 199 queryUpdated := false 200 201 if req.Query != "" { 202 if strings.HasPrefix(strings.TrimSpace(req.Query), "mutation") { 203 return nil, errors.New("client does not support mutations") 204 } 205 queryParams.Set("query", req.Query) 206 queryUpdated = true 207 } 208 209 if req.OpName != "" { 210 queryParams.Set("operationName", req.OpName) 211 queryUpdated = true 212 } 213 214 if req.Variables != nil { 215 variables, variablesErr := json.Marshal(req.Variables) 216 if variablesErr != nil { 217 return nil, variablesErr 218 } 219 queryParams.Set("variables", string(variables)) 220 queryUpdated = true 221 } 222 223 if queryUpdated { 224 parsedURL.RawQuery = queryParams.Encode() 225 } 226 227 httpReq, err := http.NewRequest( 228 c.method, 229 parsedURL.String(), 230 http.NoBody) 231 if err != nil { 232 return nil, err 233 } 234 235 return httpReq, nil 236 }