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