Skip to content

Commit

Permalink
Support explain APIs (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
hgiasac authored Oct 21, 2024
1 parent 51e3153 commit 23ac343
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The connector can automatically transform OpenAPI 2.0 and 3.0 definitions to NDC
Start the connector server at http://localhost:8080 using the [JSON Placeholder](https://jsonplaceholder.typicode.com/) APIs.

```go
go run . serve --configuration ./rest/testdata/jsonplaceholder
go run ./server serve --configuration ./connector/testdata/jsonplaceholder
```

## How it works
Expand Down
2 changes: 1 addition & 1 deletion connector-definition/config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
files:
- file: https://raw.githubusercontent.com/hasura/ndc-rest/main/rest/testdata/jsonplaceholder/swagger.json
- file: https://raw.githubusercontent.com/hasura/ndc-rest/main/connector/testdata/jsonplaceholder/swagger.json
spec: openapi2
10 changes: 0 additions & 10 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,6 @@ func (c *RESTConnector) GetCapabilities(configuration *configuration.Configurati
return c.capabilities
}

// QueryExplain explains a query by creating an execution plan.
func (c *RESTConnector) QueryExplain(ctx context.Context, configuration *configuration.Configuration, state *State, request *schema.QueryRequest) (*schema.ExplainResponse, error) {
return nil, schema.NotSupportedError("query explain has not been supported yet", nil)
}

// MutationExplain explains a mutation by creating an execution plan.
func (c *RESTConnector) MutationExplain(ctx context.Context, configuration *configuration.Configuration, state *State, request *schema.MutationRequest) (*schema.ExplainResponse, error) {
return nil, schema.NotSupportedError("mutation explain has not been supported yet", nil)
}

func parseConfiguration(configurationDir string) (*configuration.Configuration, error) {
var config configuration.Configuration
jsonBytes, err := os.ReadFile(configurationDir + "/config.json")
Expand Down
124 changes: 79 additions & 45 deletions connector/connector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,33 @@ func TestRESTConnector_authentication(t *testing.T) {
testServer := connServer.BuildTestServer()
defer testServer.Close()

t.Run("auth_default", func(t *testing.T) {
reqBody := []byte(`{
"collection": "findPets",
"query": {
"fields": {
"__value": {
"type": "column",
"column": "__value"
}
findPetsBody := []byte(`{
"collection": "findPets",
"query": {
"fields": {
"__value": {
"type": "column",
"column": "__value"
}
}
},
"arguments": {},
"collection_relationships": {}
}`)

t.Run("auth_default_explain", func(t *testing.T) {
res, err := http.Post(fmt.Sprintf("%s/query/explain", testServer.URL), "application/json", bytes.NewBuffer(findPetsBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{
Details: schema.ExplainResponseDetails{
"url": server.URL + "/pet",
"headers": `{"Api_key":["ran*******(14)"]}`,
},
"arguments": {},
"collection_relationships": {}
}`)
})
})

res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(reqBody))
t.Run("auth_default", func(t *testing.T) {
res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(findPetsBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{
{
Expand All @@ -139,23 +150,35 @@ func TestRESTConnector_authentication(t *testing.T) {
})
})

t.Run("auth_api_key", func(t *testing.T) {
reqBody := []byte(`{
"operations": [
{
"type": "procedure",
"name": "addPet",
"arguments": {
"body": {
"name": "pet"
}
addPetBody := []byte(`{
"operations": [
{
"type": "procedure",
"name": "addPet",
"arguments": {
"body": {
"name": "pet"
}
}
],
"collection_relationships": {}
}`)
}
],
"collection_relationships": {}
}`)

res, err := http.Post(fmt.Sprintf("%s/mutation", testServer.URL), "application/json", bytes.NewBuffer(reqBody))
t.Run("auth_api_key_explain", func(t *testing.T) {
res, err := http.Post(fmt.Sprintf("%s/mutation/explain", testServer.URL), "application/json", bytes.NewBuffer(addPetBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{
Details: schema.ExplainResponseDetails{
"url": server.URL + "/pet",
"headers": `{"Api_key":["ran*******(14)"],"Content-Type":["application/json"]}`,
"body": `{"name":"pet"}`,
},
})
})

t.Run("auth_api_key", func(t *testing.T) {
res, err := http.Post(fmt.Sprintf("%s/mutation", testServer.URL), "application/json", bytes.NewBuffer(addPetBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.MutationResponse{
OperationResults: []schema.MutationOperationResults{
Expand All @@ -164,28 +187,39 @@ func TestRESTConnector_authentication(t *testing.T) {
})
})

t.Run("auth_bearer", func(t *testing.T) {
reqBody := []byte(`{
"collection": "findPetsByStatus",
"query": {
"fields": {
"__value": {
"type": "column",
"column": "__value"
}
}
},
"arguments": {
"status": {
"type": "literal",
"value": "available"
authBearerBody := []byte(`{
"collection": "findPetsByStatus",
"query": {
"fields": {
"__value": {
"type": "column",
"column": "__value"
}
}
},
"arguments": {
"status": {
"type": "literal",
"value": "available"
}
},
"collection_relationships": {}
}`)

t.Run("auth_bearer_explain", func(t *testing.T) {
res, err := http.Post(fmt.Sprintf("%s/query/explain", testServer.URL), "application/json", bytes.NewBuffer(authBearerBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{
Details: schema.ExplainResponseDetails{
"url": server.URL + "/pet/findByStatus?status=available",
"headers": `{"Authorization":["Bearer ran*******(19)"]}`,
},
"collection_relationships": {}
}`)
})
})

t.Run("auth_bearer", func(t *testing.T) {
for i := 0; i < 2; i++ {
res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(reqBody))
res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(authBearerBody))
assert.NilError(t, err)
assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{
{
Expand Down
2 changes: 1 addition & 1 deletion connector/internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (client *HTTPClient) SetTracer(tracer *connector.Tracer) {

// Send creates and executes the request and evaluate response selection
func (client *HTTPClient) Send(ctx context.Context, request *RetryableRequest, selection schema.NestedField, resultType schema.Type, restOptions *RESTOptions) (any, error) {
requests, err := buildDistributedRequestsWithOptions(request, restOptions)
requests, err := BuildDistributedRequestsWithOptions(request, restOptions)
if err != nil {
return nil, err
}
Expand Down
23 changes: 12 additions & 11 deletions connector/internal/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,17 @@ func getHostFromServers(servers []rest.ServerConfig, serverIDs []string) (string
}
}

func buildDistributedRequestsWithOptions(request *RetryableRequest, restOptions *RESTOptions) ([]RetryableRequest, error) {
// BuildDistributedRequestsWithOptions builds distributed requests with options
func BuildDistributedRequestsWithOptions(request *RetryableRequest, restOptions *RESTOptions) ([]RetryableRequest, error) {
if strings.HasPrefix(request.URL, "http") {
return []RetryableRequest{*request}, nil
}

if !restOptions.Distributed || len(restOptions.Settings.Servers) == 1 {
host, serverID := getHostFromServers(restOptions.Settings.Servers, restOptions.Servers)
request.URL = fmt.Sprintf("%s%s", host, request.URL)
request.URL = host + request.URL
request.ServerID = serverID
if err := request.applySettings(restOptions.Settings); err != nil {
if err := request.applySettings(restOptions.Settings, restOptions.Explain); err != nil {
return nil, err
}
return []RetryableRequest{*request}, nil
Expand Down Expand Up @@ -125,7 +126,7 @@ func buildDistributedRequestsWithOptions(request *RetryableRequest, restOptions
Headers: request.Headers.Clone(),
Body: request.Body,
}
if err := req.applySettings(restOptions.Settings); err != nil {
if err := req.applySettings(restOptions.Settings, restOptions.Explain); err != nil {
return nil, err
}
if len(buf) > 0 {
Expand All @@ -152,7 +153,7 @@ func (req *RetryableRequest) getServerConfig(settings *rest.NDCRestSettings) *re
return nil
}

func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig) error {
func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig, isExplain bool) error {
if serverConfig == nil {
return nil
}
Expand Down Expand Up @@ -202,7 +203,7 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig) erro
if securityScheme.Value != nil {
v := securityScheme.Value.Value()
if v != nil {
req.Headers.Set(headerName, fmt.Sprintf("%s %s", scheme, *v))
req.Headers.Set(headerName, fmt.Sprintf("%s %s", scheme, eitherMaskSecret(*v, isExplain)))
}
}
case rest.APIKeyScheme:
Expand All @@ -211,7 +212,7 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig) erro
if securityScheme.Value != nil {
value := securityScheme.Value.Value()
if value != nil {
req.Headers.Set(securityScheme.Name, *value)
req.Headers.Set(securityScheme.Name, eitherMaskSecret(*value, isExplain))
}
}
case rest.APIKeyInQuery:
Expand All @@ -223,15 +224,15 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig) erro
}

q := endpoint.Query()
q.Add(securityScheme.Name, *securityScheme.Value.Value())
q.Add(securityScheme.Name, eitherMaskSecret(*value, isExplain))
endpoint.RawQuery = q.Encode()
req.URL = endpoint.String()
}
case rest.APIKeyInCookie:
if securityScheme.Value != nil {
v := securityScheme.Value.Value()
if v != nil {
req.Headers.Set("Cookie", fmt.Sprintf("%s=%s", securityScheme.Name, *v))
req.Headers.Set("Cookie", fmt.Sprintf("%s=%s", securityScheme.Name, eitherMaskSecret(*v, isExplain)))
}
}
default:
Expand All @@ -244,7 +245,7 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig) erro
return nil
}

func (req *RetryableRequest) applySettings(settings *rest.NDCRestSettings) error {
func (req *RetryableRequest) applySettings(settings *rest.NDCRestSettings, isExplain bool) error {
if req.Retry == nil {
req.Retry = &rest.RetryPolicy{}
}
Expand All @@ -255,7 +256,7 @@ func (req *RetryableRequest) applySettings(settings *rest.NDCRestSettings) error
if serverConfig == nil {
return nil
}
if err := req.applySecurity(serverConfig); err != nil {
if err := req.applySecurity(serverConfig, isExplain); err != nil {
return err
}

Expand Down
1 change: 1 addition & 0 deletions connector/internal/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type RESTOptions struct {
Servers []string `json:"servers" yaml:"serverIds"`
Parallel bool `json:"parallel" yaml:"parallel"`

Explain bool `json:"-" yaml:"-"`
Distributed bool `json:"-" yaml:"-"`
Settings *rest.NDCRestSettings `json:"-" yaml:"-"`
}
Expand Down
22 changes: 22 additions & 0 deletions connector/internal/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"fmt"
"strings"

"github.com/hasura/ndc-sdk-go/schema"
)
Expand All @@ -21,3 +22,24 @@ func UnwrapNullableType(input schema.Type) (schema.TypeEncoder, bool, error) {
return nil, false, fmt.Errorf("invalid type %v", input)
}
}

// either masks the string value for security
func eitherMaskSecret(input string, shouldMask bool) string {
if !shouldMask {
return input
}
return MaskString(input)
}

// MaskString masks the string value for security
func MaskString(input string) string {
inputLength := len(input)
switch {
case inputLength < 6:
return strings.Repeat("*", inputLength)
case inputLength < 12:
return input[0:1] + strings.Repeat("*", inputLength-1)
default:
return input[0:3] + strings.Repeat("*", 7) + fmt.Sprintf("(%d)", inputLength)
}
}
Loading

0 comments on commit 23ac343

Please sign in to comment.