Repository: newrelic/newrelic-client-go
Pull request #147 (2020-02-27 17:30:23)

feat(nerdgraph): implement ability to make raw GraphQL query
Resolves: #149 

### POC for raw GraphQL via client

First pass at adding the ability to pass raw GraphQL queries directly via a client method. 

This facilitates being able to execute a CLI command that passes a raw GraphQL query to the client.

#### CLI command example (basic query, no variables)
```
newrelic graphql query "{ actor { user { email id name } } }"
```

#### CLI command example (complex, with variables)
```
newrelic graphql query 'query($guid: EntityGuid!) { actor { entity(guid: $guid) { guid name domain entityType } } }' --variables '{"guid": "MjUyMDUyOHxBUE18QVBQTElDQVRJT058MjE1MDM3Nzk1"}'
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b600f92..1251271 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,6 @@
 ## [Unreleased]
 
 
-<a name="v0.11.0"></a>
-## [v0.11.0] - 2020-02-27
-### Features
-- **http:** allow personal API keys to be used for alerts and APM resources
-
-
 <a name="v0.10.1"></a>
 ## [v0.10.1] - 2020-02-20
 ### Bug Fixes
@@ -196,8 +190,7 @@
 - get TestListApplications passing
 
 
-[Unreleased]: https://github.com/newrelic/newrelic-client-go/compare/v0.11.0...HEAD
-[v0.11.0]: https://github.com/newrelic/newrelic-client-go/compare/v0.10.1...v0.11.0
+[Unreleased]: https://github.com/newrelic/newrelic-client-go/compare/v0.10.1...HEAD
 [v0.10.1]: https://github.com/newrelic/newrelic-client-go/compare/v0.10.0...v0.10.1
 [v0.10.0]: https://github.com/newrelic/newrelic-client-go/compare/v0.9.0...v0.10.0
 [v0.9.0]: https://github.com/newrelic/newrelic-client-go/compare/v0.8.0...v0.9.0
diff --git a/internal/http/auth.go b/internal/http/auth.go
deleted file mode 100644
index 3e72327..0000000
--- a/internal/http/auth.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package http
-
-import (
-	retryablehttp "github.com/hashicorp/go-retryablehttp"
-	"github.com/newrelic/newrelic-client-go/pkg/config"
-)
-
-// RequestAuthorizer is an interface that allows customizatino of how a request is authorized.
-type RequestAuthorizer interface {
-	AuthorizeRequest(*retryablehttp.Request, *config.Config)
-}
-
-// NerdGraphAuthorizer authorizes calls to NerdGraph.
-type NerdGraphAuthorizer struct{}
-
-// AuthorizeRequest is responsible for setting up auth for a request.
-func (a *NerdGraphAuthorizer) AuthorizeRequest(req *retryablehttp.Request, c *config.Config) {
-	req.Header.Set("Api-Key", c.PersonalAPIKey)
-}
-
-// PersonalAPIKeyCapableV2Authorizer authorizes V2 endpoints that can use a personal API key.
-type PersonalAPIKeyCapableV2Authorizer struct{}
-
-// AuthorizeRequest is responsible for setting up auth for a request.
-func (a *PersonalAPIKeyCapableV2Authorizer) AuthorizeRequest(req *retryablehttp.Request, c *config.Config) {
-	if c.PersonalAPIKey != "" {
-		req.Header.Set("Api-Key", c.PersonalAPIKey)
-		req.Header.Set("Auth-Type", "User-Api-Key")
-	} else {
-		req.Header.Set("X-Api-Key", c.APIKey)
-	}
-}
-
-// ClassicV2Authorizer authorizes V2 endpoints that cannot use a personal API key.
-type ClassicV2Authorizer struct{}
-
-// AuthorizeRequest is responsible for setting up auth for a request.
-func (a *ClassicV2Authorizer) AuthorizeRequest(req *retryablehttp.Request, c *config.Config) {
-	req.Header.Set("X-Api-Key", c.APIKey)
-}
diff --git a/internal/http/client.go b/internal/http/client.go
index a6f7502..c6fae79 100644
--- a/internal/http/client.go
+++ b/internal/http/client.go
@@ -32,15 +32,8 @@ var (
 
 // NewRelicClient represents a client for communicating with the New Relic APIs.
 type NewRelicClient struct {
-	// Client represents the underlying HTTP client.
-	Client *retryablehttp.Client
-
-	// Config is the HTTP client configuration.
-	Config config.Config
-
-	// UsePersonalAPIKeyCompatability is for internal use only.
-	AuthStrategy RequestAuthorizer
-
+	Client     *retryablehttp.Client
+	Config     config.Config
 	errorValue ErrorResponse
 }
 
@@ -86,10 +79,9 @@ func NewClient(cfg config.Config) NewRelicClient {
 	r.Logger = nil
 
 	return NewRelicClient{
-		Client:       r,
-		Config:       cfg,
-		errorValue:   &DefaultErrorResponse{},
-		AuthStrategy: &ClassicV2Authorizer{},
+		Client:     r,
+		Config:     cfg,
+		errorValue: &DefaultErrorResponse{},
 	}
 }
 
@@ -203,6 +195,14 @@ func (c *NewRelicClient) setHeaders(req *retryablehttp.Request) {
 	req.Header.Set(defaultNewRelicRequestingServiceHeader, defaultServiceName)
 	req.Header.Set("Content-Type", "application/json")
 	req.Header.Set("User-Agent", c.Config.UserAgent)
+
+	if c.Config.APIKey != "" {
+		req.Header.Set("X-Api-Key", c.Config.APIKey)
+	}
+
+	if c.Config.PersonalAPIKey != "" {
+		req.Header.Set("Api-Key", c.Config.PersonalAPIKey)
+	}
 }
 
 func setQueryParams(req *retryablehttp.Request, params interface{}) error {
@@ -260,8 +260,6 @@ func (c *NewRelicClient) do(
 	}
 
 	c.setHeaders(req)
-	c.AuthStrategy.AuthorizeRequest(req, &c.Config)
-
 	err = setQueryParams(req, params)
 	if err != nil {
 		return nil, err
diff --git a/internal/http/graphql_client.go b/internal/http/graphql_client.go
index 618de3f..f1d4d4e 100644
--- a/internal/http/graphql_client.go
+++ b/internal/http/graphql_client.go
@@ -24,7 +24,6 @@ func NewGraphQLClient(cfg config.Config) *GraphQLClient {
 	cfg.BaseURL = cfg.NerdGraphBaseURL
 
 	c := NewClient(cfg)
-	c.AuthStrategy = &NerdGraphAuthorizer{}
 	c.SetErrorValue(&graphQLErrorResponse{})
 
 	return &GraphQLClient{
diff --git a/internal/version/version.go b/internal/version/version.go
index 8cd0602..b90ef6c 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -1,4 +1,4 @@
 package version
 
 // Version of this library
-const Version string = "0.11.0"
+const Version string = "0.10.1"
diff --git a/newrelic/newrelic.go b/newrelic/newrelic.go
index a08677f..830d2cb 100644
--- a/newrelic/newrelic.go
+++ b/newrelic/newrelic.go
@@ -11,6 +11,7 @@ import (
 	"github.com/newrelic/newrelic-client-go/pkg/config"
 	"github.com/newrelic/newrelic-client-go/pkg/dashboards"
 	"github.com/newrelic/newrelic-client-go/pkg/entities"
+	"github.com/newrelic/newrelic-client-go/pkg/nerdgraph"
 	"github.com/newrelic/newrelic-client-go/pkg/plugins"
 	"github.com/newrelic/newrelic-client-go/pkg/synthetics"
 )
@@ -23,6 +24,7 @@ type NewRelic struct {
 	Entities   entities.Entities
 	Plugins    plugins.Plugins
 	Synthetics synthetics.Synthetics
+	NerdGraph  nerdgraph.NerdGraph
 }
 
 // New returns a collection of New Relic APIs.
@@ -49,6 +51,7 @@ func New(opts ...ConfigOption) (*NewRelic, error) {
 		Entities:   entities.New(config),
 		Plugins:    plugins.New(config),
 		Synthetics: synthetics.New(config),
+		NerdGraph:  nerdgraph.New(config),
 	}
 
 	return nr, nil
diff --git a/pkg/alerts/alerts.go b/pkg/alerts/alerts.go
index 532270a..2d5ddac 100644
--- a/pkg/alerts/alerts.go
+++ b/pkg/alerts/alerts.go
@@ -29,11 +29,8 @@ func New(config config.Config) Alerts {
 	infraClient := http.NewClient(infraConfig)
 	infraClient.SetErrorValue(&infrastructure.ErrorResponse{})
 
-	client := http.NewClient(config)
-	client.AuthStrategy = &http.PersonalAPIKeyCapableV2Authorizer{}
-
 	pkg := Alerts{
-		client:      client,
+		client:      http.NewClient(config),
 		infraClient: infraClient,
 		logger:      config.GetLogger(),
 		pager:       &http.LinkHeaderPager{},
diff --git a/pkg/apm/apm.go b/pkg/apm/apm.go
index 1e20fe4..5d84117 100644
--- a/pkg/apm/apm.go
+++ b/pkg/apm/apm.go
@@ -16,11 +16,9 @@ type APM struct {
 
 // New is used to create a new APM client instance.
 func New(config config.Config) APM {
-	client := http.NewClient(config)
-	client.AuthStrategy = &http.PersonalAPIKeyCapableV2Authorizer{}
-
 	pkg := APM{
-		client: client,
+		client: http.NewClient(config),
+		logger: config.GetLogger(),
 		pager:  &http.LinkHeaderPager{},
 	}
 
diff --git a/pkg/nerdgraph/nerdgraph.go b/pkg/nerdgraph/nerdgraph.go
new file mode 100644
index 0000000..a1b3f62
--- /dev/null
+++ b/pkg/nerdgraph/nerdgraph.go
@@ -0,0 +1,39 @@
+package nerdgraph
+
+import (
+	"github.com/newrelic/newrelic-client-go/internal/http"
+	"github.com/newrelic/newrelic-client-go/internal/logging"
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+)
+
+// NerdGraph is used to communicate with the New Relic's GraphQL API, NerdGraph.
+type NerdGraph struct {
+	client *http.GraphQLClient
+	logger logging.Logger
+}
+
+// New returns a new GraphQL client for interacting with New Relic's GraphQL API, NerdGraph.
+func New(config config.Config) NerdGraph {
+	return NerdGraph{
+		client: http.NewGraphQLClient(config),
+		logger: config.GetLogger(),
+	}
+}
+
+// Query facilitates making a NerdGraph request with a raw GraphQL query. Variables may be provided
+// in the form of a map. The response's data structure will vary based on the query provided.
+func (n *NerdGraph) Query(query string, variables map[string]interface{}) (interface{}, error) {
+	respBody := queryResponse{}
+
+	if err := n.client.Query(query, variables, &respBody); err != nil {
+		return nil, err
+	}
+
+	return respBody, nil
+}
+
+type queryResponse struct {
+	Actor          interface{} `json:"actor,omitempty"`
+	Docs           interface{} `json:"docs,omitempty"`
+	RequestContext interface{} `json:"requestContext,omitempty"`
+}


Test patch:
diff --git a/internal/http/client_test.go b/internal/http/client_test.go
index 50a8e03..0c250c7 100644
--- a/internal/http/client_test.go
+++ b/internal/http/client_test.go
@@ -5,7 +5,6 @@ package http
 import (
 	"encoding/json"
 	"net/http"
-	"net/http/httptest"
 	"testing"
 	"time"
 
@@ -232,6 +231,8 @@ func TestHeaders(t *testing.T) {
 		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(http.StatusOK)
 
+		assert.Equal(t, testAPIKey, r.Header.Get("x-api-key"))
+		assert.Equal(t, testPersonalAPIKey, r.Header.Get("api-key"))
 		assert.Equal(t, testUserAgent, r.Header.Get("user-agent"))
 		assert.Equal(t, "newrelic-client-go", r.Header.Get("newrelic-requesting-services"))
 	}))
@@ -241,26 +242,6 @@ func TestHeaders(t *testing.T) {
 	assert.Nil(t, err)
 }
 
-func TestAdminAPIKeyHeader(t *testing.T) {
-	t.Parallel()
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-
-		assert.Equal(t, testAPIKey, r.Header.Get("x-api-key"))
-	}))
-
-	c := NewClient(config.Config{
-		APIKey:    testAPIKey,
-		BaseURL:   ts.URL,
-		UserAgent: testUserAgent,
-	})
-
-	_, err := c.Get("/path", nil, nil)
-
-	assert.Nil(t, err)
-}
-
 func TestErrNotFound(t *testing.T) {
 	t.Parallel()
 	c := NewTestAPIClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/alerts/alerts_test.go b/pkg/alerts/alerts_test.go
index a915dbd..6c6e7d7 100644
--- a/pkg/alerts/alerts_test.go
+++ b/pkg/alerts/alerts_test.go
@@ -45,19 +45,15 @@ func newMockResponse(
 // nolint
 func newIntegrationTestClient(t *testing.T) Alerts {
 	apiKey := os.Getenv("NEWRELIC_API_KEY")
-	personalAPIKey := os.Getenv("NEWRELIC_PERSONAL_API_KEY")
 
-	if apiKey == "" && personalAPIKey == "" {
-		t.Skipf("acceptance testing requires NEWRELIC_API_KEY and NEWRELIC_PERSONAL_API_KEY")
+	if apiKey == "" {
+		t.Skipf("acceptance testing requires an API key")
 	}
 
-	client := New(config.Config{
-		APIKey:         apiKey,
-		PersonalAPIKey: personalAPIKey,
-		LogLevel:       "debug",
+	return New(config.Config{
+		APIKey:   apiKey,
+		LogLevel: "debug",
 	})
-
-	return client
 }
 
 func TestSetBaseURL(t *testing.T) {
diff --git a/pkg/apm/apm_test.go b/pkg/apm/apm_test.go
index 9cc9ed8..6b997dd 100644
--- a/pkg/apm/apm_test.go
+++ b/pkg/apm/apm_test.go
@@ -42,17 +42,13 @@ func newMockResponse(
 // nolint
 func newIntegrationTestClient(t *testing.T) APM {
 	apiKey := os.Getenv("NEWRELIC_API_KEY")
-	personalAPIKey := os.Getenv("NEWRELIC_PERSONAL_API_KEY")
 
-	if apiKey == "" && personalAPIKey == "" {
-		t.Skipf("acceptance testing requires NEWRELIC_API_KEY and NEWRELIC_PERSONAL_API_KEY")
+	if apiKey == "" {
+		t.Skipf("acceptance testing requires an API key")
 	}
 
-	client := New(config.Config{
-		APIKey:         apiKey,
-		PersonalAPIKey: personalAPIKey,
-		LogLevel:       "debug",
+	return New(config.Config{
+		APIKey:   apiKey,
+		LogLevel: "debug",
 	})
-
-	return client
 }
diff --git a/pkg/nerdgraph/nerdgraph_integration_test.go b/pkg/nerdgraph/nerdgraph_integration_test.go
new file mode 100644
index 0000000..a437bd1
--- /dev/null
+++ b/pkg/nerdgraph/nerdgraph_integration_test.go
@@ -0,0 +1,76 @@
+// +build integration
+
+package nerdgraph
+
+import (
+	"os"
+	"testing"
+
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIntegrationQuery(t *testing.T) {
+	t.Parallel()
+
+	client := newNerdGraphIntegrationTestClient(t)
+
+	query := `{
+		actor {
+			user {
+				email
+				id
+				name
+			}
+		}
+	}`
+
+	variables := map[string]interface{}{}
+
+	actual, err := client.Query(query, variables)
+
+	require.NoError(t, err)
+	require.NotNil(t, actual)
+}
+
+func TestIntegrationQueryWithVariables(t *testing.T) {
+	t.Parallel()
+
+	gqlClient := newNerdGraphIntegrationTestClient(t)
+
+	query := `
+		query($guid: EntityGuid!) {
+			actor {
+				entity(guid: $guid) {
+					guid
+					name
+					domain
+					entityType
+				}
+			}
+		}
+	`
+
+	variables := map[string]interface{}{
+		"guid": "MjUyMDUyOHxBUE18QVBQTElDQVRJT058MjE1MDM3Nzk1",
+	}
+
+	actual, err := gqlClient.Query(query, variables)
+
+	require.NoError(t, err)
+	require.NotNil(t, actual)
+}
+
+// nolint
+func newNerdGraphIntegrationTestClient(t *testing.T) NerdGraph {
+	personalAPIKey := os.Getenv("NEWRELIC_PERSONAL_API_KEY")
+
+	if personalAPIKey == "" {
+		t.Skipf("acceptance testing for NerdGraph requires your personal API key")
+	}
+
+	return New(config.Config{
+		PersonalAPIKey: personalAPIKey,
+		UserAgent:      "newrelic/newrelic-client-go",
+	})
+}
diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go
index e39deb0..efbe627 100644
--- a/pkg/plugins/plugins_test.go
+++ b/pkg/plugins/plugins_test.go
@@ -45,19 +45,15 @@ func newMockResponse(
 // nolint
 func newIntegrationTestClient(t *testing.T) Plugins {
 	apiKey := os.Getenv("NEWRELIC_API_KEY")
-	personalAPIKey := os.Getenv("NEWRELIC_PERSONAL_API_KEY")
 
-	if apiKey == "" && personalAPIKey == "" {
-		t.Skipf("acceptance testing requires NEWRELIC_API_KEY and NEWRELIC_PERSONAL_API_KEY")
+	if apiKey == "" {
+		t.Skipf("acceptance testing requires an API key")
 	}
 
-	client := New(config.Config{
-		APIKey:         apiKey,
-		PersonalAPIKey: personalAPIKey,
-		LogLevel:       "debug",
+	return New(config.Config{
+		APIKey:   apiKey,
+		LogLevel: "debug",
 	})
-
-	return client
 }
 
 var (