...

Source file src/github.com/cli/go-gh/v2/pkg/api/graphql_client.go

Documentation: github.com/cli/go-gh/v2/pkg/api

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"strings"
    12  
    13  	"github.com/cli/go-gh/v2/pkg/auth"
    14  	graphql "github.com/cli/shurcooL-graphql"
    15  )
    16  
    17  // GraphQLClient wraps methods for the different types of
    18  // API requests that are supported by the server.
    19  type GraphQLClient struct {
    20  	client     *graphql.Client
    21  	host       string
    22  	httpClient *http.Client
    23  }
    24  
    25  func DefaultGraphQLClient() (*GraphQLClient, error) {
    26  	return NewGraphQLClient(ClientOptions{})
    27  }
    28  
    29  // GraphQLClient builds a client to send requests to GitHub GraphQL API endpoints.
    30  // As part of the configuration a hostname, auth token, default set of headers,
    31  // and unix domain socket are resolved from the gh environment configuration.
    32  // These behaviors can be overridden using the opts argument.
    33  func NewGraphQLClient(opts ClientOptions) (*GraphQLClient, error) {
    34  	if optionsNeedResolution(opts) {
    35  		var err error
    36  		opts, err = resolveOptions(opts)
    37  		if err != nil {
    38  			return nil, err
    39  		}
    40  	}
    41  
    42  	httpClient, err := NewHTTPClient(opts)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	endpoint := graphQLEndpoint(opts.Host)
    48  
    49  	return &GraphQLClient{
    50  		client:     graphql.NewClient(endpoint, httpClient),
    51  		host:       endpoint,
    52  		httpClient: httpClient,
    53  	}, nil
    54  }
    55  
    56  // DoWithContext executes a GraphQL query request.
    57  // The response is populated into the response argument.
    58  func (c *GraphQLClient) DoWithContext(ctx context.Context, query string, variables map[string]interface{}, response interface{}) error {
    59  	reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables})
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	req, err := http.NewRequestWithContext(ctx, "POST", c.host, bytes.NewBuffer(reqBody))
    65  	if err != nil {
    66  		return err
    67  	}
    68  
    69  	resp, err := c.httpClient.Do(req)
    70  	if err != nil {
    71  		return err
    72  	}
    73  	defer resp.Body.Close()
    74  
    75  	success := resp.StatusCode >= 200 && resp.StatusCode < 300
    76  	if !success {
    77  		return HandleHTTPError(resp)
    78  	}
    79  
    80  	if resp.StatusCode == http.StatusNoContent {
    81  		return nil
    82  	}
    83  
    84  	body, err := io.ReadAll(resp.Body)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	gr := graphQLResponse{Data: response}
    90  	err = json.Unmarshal(body, &gr)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	if len(gr.Errors) > 0 {
    96  		return &GraphQLError{Errors: gr.Errors}
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // Do wraps DoWithContext using context.Background.
   103  func (c *GraphQLClient) Do(query string, variables map[string]interface{}, response interface{}) error {
   104  	return c.DoWithContext(context.Background(), query, variables, response)
   105  }
   106  
   107  // MutateWithContext executes a GraphQL mutation request.
   108  // The mutation string is derived from the mutation argument, and the
   109  // response is populated into it.
   110  // The mutation argument should be a pointer to struct that corresponds
   111  // to the GitHub GraphQL schema.
   112  // Provided input will be set as a variable named input.
   113  func (c *GraphQLClient) MutateWithContext(ctx context.Context, name string, m interface{}, variables map[string]interface{}) error {
   114  	err := c.client.MutateNamed(ctx, name, m, variables)
   115  	var graphQLErrs graphql.Errors
   116  	if err != nil && errors.As(err, &graphQLErrs) {
   117  		items := make([]GraphQLErrorItem, len(graphQLErrs))
   118  		for i, e := range graphQLErrs {
   119  			items[i] = GraphQLErrorItem{
   120  				Message:    e.Message,
   121  				Locations:  e.Locations,
   122  				Path:       e.Path,
   123  				Extensions: e.Extensions,
   124  				Type:       e.Type,
   125  			}
   126  		}
   127  		err = &GraphQLError{items}
   128  	}
   129  	return err
   130  }
   131  
   132  // Mutate wraps MutateWithContext using context.Background.
   133  func (c *GraphQLClient) Mutate(name string, m interface{}, variables map[string]interface{}) error {
   134  	return c.MutateWithContext(context.Background(), name, m, variables)
   135  }
   136  
   137  // QueryWithContext executes a GraphQL query request,
   138  // The query string is derived from the query argument, and the
   139  // response is populated into it.
   140  // The query argument should be a pointer to struct that corresponds
   141  // to the GitHub GraphQL schema.
   142  func (c *GraphQLClient) QueryWithContext(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error {
   143  	err := c.client.QueryNamed(ctx, name, q, variables)
   144  	var graphQLErrs graphql.Errors
   145  	if err != nil && errors.As(err, &graphQLErrs) {
   146  		items := make([]GraphQLErrorItem, len(graphQLErrs))
   147  		for i, e := range graphQLErrs {
   148  			items[i] = GraphQLErrorItem{
   149  				Message:    e.Message,
   150  				Locations:  e.Locations,
   151  				Path:       e.Path,
   152  				Extensions: e.Extensions,
   153  				Type:       e.Type,
   154  			}
   155  		}
   156  		err = &GraphQLError{items}
   157  	}
   158  	return err
   159  }
   160  
   161  // Query wraps QueryWithContext using context.Background.
   162  func (c *GraphQLClient) Query(name string, q interface{}, variables map[string]interface{}) error {
   163  	return c.QueryWithContext(context.Background(), name, q, variables)
   164  }
   165  
   166  type graphQLResponse struct {
   167  	Data   interface{}
   168  	Errors []GraphQLErrorItem
   169  }
   170  
   171  func graphQLEndpoint(host string) string {
   172  	if isGarage(host) {
   173  		return fmt.Sprintf("https://%s/api/graphql", host)
   174  	}
   175  	host = auth.NormalizeHostname(host)
   176  	if auth.IsEnterprise(host) {
   177  		return fmt.Sprintf("https://%s/api/graphql", host)
   178  	}
   179  	if strings.EqualFold(host, localhost) {
   180  		return fmt.Sprintf("http://api.%s/graphql", host)
   181  	}
   182  	return fmt.Sprintf("https://api.%s/graphql", host)
   183  }
   184  

View as plain text