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  }