github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/third_party/machinebox/graphql/graphql.go (about) 1 // Package graphql provides a low level GraphQL client. 2 // 3 // // create a client (safe to share across requests) 4 // client := graphql.NewClient("https://machinebox.io/graphql") 5 // 6 // // make a request 7 // req := graphql.NewRequest(` 8 // query ($key: String!) { 9 // items (id:$key) { 10 // field1 11 // field2 12 // field3 13 // } 14 // } 15 // `) 16 // 17 // // set any variables 18 // req.Var("key", "value") 19 // 20 // // run it and capture the response 21 // var respData ResponseStruct 22 // if err := client.Run(ctx, req, &respData); err != nil { 23 // log.Fatal(err) 24 // } 25 // 26 // # Specify client 27 // 28 // To specify your own http.Client, use the WithHTTPClient option: 29 // 30 // httpclient := &http.Client{} 31 // client := graphql.NewClient("https://machinebox.io/graphql", graphql.WithHTTPClient(httpclient)) 32 package graphql 33 34 import ( 35 "bytes" 36 "context" 37 "encoding/json" 38 "fmt" 39 "io" 40 "mime/multipart" 41 "net/http" 42 ) 43 44 // Client is a client for interacting with a GraphQL API. 45 type Client struct { 46 endpoint string 47 httpClient *http.Client 48 useMultipartForm bool 49 50 // closeReq will close the request body immediately allowing for reuse of client 51 closeReq bool 52 53 // Log is called with various debug information. 54 // To log to standard out, use: 55 // client.Log = func(s string) { log.Println(s) } 56 Log func(s string) 57 } 58 59 // NewClient makes a new Client capable of making GraphQL requests. 60 func NewClient(endpoint string, opts ...ClientOption) *Client { 61 c := &Client{ 62 endpoint: endpoint, 63 Log: func(string) {}, 64 } 65 for _, optionFunc := range opts { 66 optionFunc(c) 67 } 68 if c.httpClient == nil { 69 c.httpClient = http.DefaultClient 70 } 71 return c 72 } 73 74 func (c *Client) logf(format string, args ...interface{}) { 75 c.Log(fmt.Sprintf(format, args...)) 76 } 77 78 // Run executes the query and unmarshals the response from the data field 79 // into the response object. 80 // Pass in a nil response object to skip response parsing. 81 // If the request fails or the server returns an error, the first error 82 // will be returned. 83 func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error { 84 select { 85 case <-ctx.Done(): 86 return ctx.Err() 87 default: 88 } 89 if len(req.files) > 0 && !c.useMultipartForm { 90 return fmt.Errorf("cannot send files with PostFields option") 91 } 92 if c.useMultipartForm { 93 return c.runWithPostFields(ctx, req, resp) 94 } 95 return c.runWithJSON(ctx, req, resp) 96 } 97 98 func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}) error { 99 var requestBody bytes.Buffer 100 requestBodyObj := struct { 101 Query string `json:"query"` 102 Variables map[string]interface{} `json:"variables"` 103 }{ 104 Query: req.q, 105 Variables: req.vars, 106 } 107 if err := json.NewEncoder(&requestBody).Encode(requestBodyObj); err != nil { 108 return fmt.Errorf("encode body: %w", err) 109 } 110 c.logf(">> variables: %v", req.vars) 111 c.logf(">> query: %s", req.q) 112 gr := &graphResponse{ 113 Data: resp, 114 } 115 r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody) 116 if err != nil { 117 return err 118 } 119 r.Close = c.closeReq 120 r.Header.Set("Content-Type", "application/json; charset=utf-8") 121 r.Header.Set("Accept", "application/json; charset=utf-8") 122 for key, values := range req.Header { 123 for _, value := range values { 124 r.Header.Add(key, value) 125 } 126 } 127 c.logf(">> headers: %v", r.Header) 128 r = r.WithContext(ctx) 129 res, err := c.httpClient.Do(r) 130 if err != nil { 131 return err 132 } 133 defer res.Body.Close() 134 var buf bytes.Buffer 135 if _, err := io.Copy(&buf, res.Body); err != nil { 136 return fmt.Errorf("copy body: %w", err) 137 } 138 c.logf("<< %s", buf.String()) 139 if err := json.NewDecoder(&buf).Decode(&gr); err != nil { 140 if res.StatusCode != http.StatusOK { 141 return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode) 142 } 143 return fmt.Errorf("decode body: %w", err) 144 } 145 if len(gr.Errors) > 0 { 146 // return first error 147 return gr.Errors[0] 148 } 149 if len(gr.Errors) > 0 { 150 // return first error 151 return gr.Errors[0] 152 } 153 return nil 154 } 155 156 func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp interface{}) error { 157 var requestBody bytes.Buffer 158 writer := multipart.NewWriter(&requestBody) 159 if err := writer.WriteField("query", req.q); err != nil { 160 return fmt.Errorf("write query field: %w", err) 161 } 162 var variablesBuf bytes.Buffer 163 if len(req.vars) > 0 { 164 variablesField, err := writer.CreateFormField("variables") 165 if err != nil { 166 return fmt.Errorf("create variables field: %w", err) 167 } 168 if err := json.NewEncoder(io.MultiWriter(variablesField, &variablesBuf)).Encode(req.vars); err != nil { 169 return fmt.Errorf("encode variables: %w", err) 170 } 171 } 172 for i := range req.files { 173 part, err := writer.CreateFormFile(req.files[i].Field, req.files[i].Name) 174 if err != nil { 175 return fmt.Errorf("create form file: %w", err) 176 } 177 if _, err := io.Copy(part, req.files[i].R); err != nil { 178 return fmt.Errorf("copy file: %w", err) 179 } 180 } 181 if err := writer.Close(); err != nil { 182 return fmt.Errorf("close writer: %w", err) 183 } 184 c.logf(">> variables: %s", variablesBuf.String()) 185 c.logf(">> files: %d", len(req.files)) 186 c.logf(">> query: %s", req.q) 187 gr := &graphResponse{ 188 Data: resp, 189 } 190 r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody) 191 if err != nil { 192 return err 193 } 194 r.Close = c.closeReq 195 r.Header.Set("Content-Type", writer.FormDataContentType()) 196 r.Header.Set("Accept", "application/json; charset=utf-8") 197 for key, values := range req.Header { 198 for _, value := range values { 199 r.Header.Add(key, value) 200 } 201 } 202 c.logf(">> headers: %v", r.Header) 203 r = r.WithContext(ctx) 204 res, err := c.httpClient.Do(r) 205 if err != nil { 206 return err 207 } 208 defer res.Body.Close() 209 var buf bytes.Buffer 210 if _, err := io.Copy(&buf, res.Body); err != nil { 211 return fmt.Errorf("copy body: %w", err) 212 } 213 c.logf("<< %s", buf.String()) 214 if err := json.NewDecoder(&buf).Decode(&gr); err != nil { 215 if res.StatusCode != http.StatusOK { 216 return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode) 217 } 218 return fmt.Errorf("decoding response: %w", err) 219 } 220 if len(gr.Errors) > 0 { 221 // return first error 222 return gr.Errors[0] 223 } 224 return nil 225 } 226 227 // WithHTTPClient specifies the underlying http.Client to use when 228 // making requests. 229 // 230 // NewClient(endpoint, WithHTTPClient(specificHTTPClient)) 231 func WithHTTPClient(httpclient *http.Client) ClientOption { 232 return func(client *Client) { 233 client.httpClient = httpclient 234 } 235 } 236 237 // UseMultipartForm uses multipart/form-data and activates support for 238 // files. 239 func UseMultipartForm() ClientOption { 240 return func(client *Client) { 241 client.useMultipartForm = true 242 } 243 } 244 245 // ImmediatelyCloseReqBody will close the req body immediately after each request body is ready 246 func ImmediatelyCloseReqBody() ClientOption { 247 return func(client *Client) { 248 client.closeReq = true 249 } 250 } 251 252 // ClientOption are functions that are passed into NewClient to 253 // modify the behaviour of the Client. 254 type ClientOption func(*Client) 255 256 type ExtendedError interface { 257 Error() string 258 Extensions() map[string]interface{} 259 } 260 261 type graphErr struct { 262 Message string `json:"message,omitempty"` 263 ErrorExtensions map[string]interface{} `json:"extensions,omitempty"` 264 } 265 266 func (e graphErr) Error() string { 267 return "graphql: " + e.Message 268 } 269 270 func (e graphErr) Extensions() map[string]interface{} { 271 return e.ErrorExtensions 272 } 273 274 type graphResponse struct { 275 Data interface{} 276 Errors []graphErr 277 } 278 279 // Request is a GraphQL request. 280 type Request struct { 281 q string 282 vars map[string]interface{} 283 files []File 284 285 // Header represent any request headers that will be set 286 // when the request is made. 287 Header http.Header 288 } 289 290 // NewRequest makes a new Request with the specified string. 291 func NewRequest(q string) *Request { 292 req := &Request{ 293 q: q, 294 Header: make(map[string][]string), 295 } 296 return req 297 } 298 299 // Var sets a variable. 300 func (req *Request) Var(key string, value interface{}) { 301 if req.vars == nil { 302 req.vars = make(map[string]interface{}) 303 } 304 req.vars[key] = value 305 } 306 307 // Vars gets the variables for this Request. 308 func (req *Request) Vars() map[string]interface{} { 309 return req.vars 310 } 311 312 // Files gets the files in this request. 313 func (req *Request) Files() []File { 314 return req.files 315 } 316 317 // Query gets the query string of this request. 318 func (req *Request) Query() string { 319 return req.q 320 } 321 322 // File sets a file to upload. 323 // Files are only supported with a Client that was created with 324 // the UseMultipartForm option. 325 func (req *Request) File(fieldname, filename string, r io.Reader) { 326 req.files = append(req.files, File{ 327 Field: fieldname, 328 Name: filename, 329 R: r, 330 }) 331 } 332 333 // File represents a file to upload. 334 type File struct { 335 Field string 336 Name string 337 R io.Reader 338 }