Skip to content

Commit

Permalink
[TT-8526] fix issue where query params of type int and array are wrap…
Browse files Browse the repository at this point in the history
…ped with quotes (#355)

THis ticket fixes an issue where the input generated for the rest
datasource wraps query params that are fetched from arguments with
quotes, thereby breaking the URL generated down the execution pipeline
  • Loading branch information
kofoworola committed Jun 22, 2023
1 parent 849c158 commit 19fa44a
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 10 deletions.
58 changes: 49 additions & 9 deletions pkg/engine/datasource/rest_datasource/rest_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/buger/jsonparser"
"io"
"net/http"
"regexp"
Expand All @@ -23,6 +25,10 @@ type Planner struct {
operationDefinition int
}

const (
typeString = "String"
)

func (p *Planner) DownstreamResponseFieldAlias(_ int) (alias string, exists bool) {
// the REST DataSourcePlanner doesn't rewrite upstream fields: skip
return
Expand Down Expand Up @@ -65,16 +71,17 @@ type SubscriptionConfiguration struct {
}

type FetchConfiguration struct {
URL string
Method string
Header http.Header
Query []QueryConfiguration
Body string
URL string
Method string
Header http.Header
Query []QueryConfiguration
Body string
}

type QueryConfiguration struct {
Name string `json:"name"`
Value string `json:"value"`
Name string `json:"name"`
Value string `json:"value"`
rawMessage json.RawMessage
}

func (p *Planner) Register(visitor *plan.Visitor, configuration plan.DataSourceConfiguration, isNested bool) error {
Expand All @@ -100,7 +107,7 @@ func (p *Planner) configureInput() []byte {
}

preparedQuery := p.prepareQueryParams(p.rootField, p.config.Fetch.Query)
query, err := json.Marshal(preparedQuery)
query, err := p.marshalQueryParams(preparedQuery)
if err == nil && len(preparedQuery) != 0 {
input = httpclient.SetInputQueryParams(input, query)
}
Expand All @@ -124,7 +131,7 @@ func (p *Planner) ConfigureSubscription() plan.SubscriptionConfiguration {
}

var (
selectorRegex = regexp.MustCompile(`{{\s(.*?)\s}}`)
selectorRegex = regexp.MustCompile(`{{\s?(.*?)\s?}}`)
)

func (p *Planner) prepareQueryParams(field int, query []QueryConfiguration) []QueryConfiguration {
Expand Down Expand Up @@ -152,6 +159,21 @@ Next:
if value.Kind != ast.ValueKindVariable {
continue Next
}

variableDefRef, exists := p.v.Operation.VariableDefinitionByNameAndOperation(p.operationDefinition, p.v.Operation.VariableValueNameBytes(value.Ref))
if !exists {
continue
}
typeRef := p.v.Operation.VariableDefinitions[variableDefRef].Type
typeName := p.v.Operation.TypeNameString(typeRef)
typeKind := p.v.Operation.Types[typeRef].TypeKind
// if type is a nullable or non-nullable string, add quotes to the raw message
if typeName == typeString || (typeKind == ast.TypeKindNonNull && p.v.Operation.TypeNameString(p.v.Operation.Types[typeRef].OfType) == typeString) {
query[i].rawMessage = []byte(`"` + query[i].Value + `"`)
} else {
query[i].rawMessage = []byte(query[i].Value)
}

variableName := p.v.Operation.VariableValueNameString(value.Ref)
if !p.v.Operation.OperationDefinitionHasVariableDefinition(p.operationDefinition, variableName) {
continue Next
Expand All @@ -163,6 +185,24 @@ Next:
return out
}

func (p *Planner) marshalQueryParams(params []QueryConfiguration) ([]byte, error) {
marshalled, err := json.Marshal(params)
if err != nil {
return nil, err
}
for i := range params {
if params[i].rawMessage != nil {
marshalled, err = jsonparser.Set(marshalled, params[i].rawMessage, fmt.Sprintf("[%d]", i), "value")
} else {
marshalled, err = jsonparser.Set(marshalled, []byte(`"`+params[i].Value+`"`), fmt.Sprintf("[%d]", i), "value")
}
if err != nil {
return nil, err
}
}
return marshalled, nil
}

type Source struct {
client *http.Client
}
Expand Down
248 changes: 247 additions & 1 deletion pkg/engine/datasource/rest_datasource/rest_datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const (
friend: Friend
withArgument(id: String!, name: String, optional: String): Friend
withArrayArguments(names: [String]): Friend
withIntArgument(limit: Int): Friend
withStringArgument(name: String!): Friend
}
type Subscription {
Expand All @@ -45,6 +47,7 @@ const (
name: String
pet: Pet
phone(name: String!): String
hasArg(limit: Int!): String
}
type Pet {
Expand Down Expand Up @@ -119,6 +122,30 @@ const (
}
`

intArgumentOperation = `
query ArgumentQuery {
withIntArgument(limit: 10) {
name
}
}
`

intArgumentOperationNonNullableInt = `
query ArgumentQuery($in: Int!) {
withIntArgument(limit: $in) {
name
}
}
`

stringArgumentOperationNonNullableString = `
query ArgumentQuery($in: String!) {
withStringArgument(name: $in) {
name
}
}
`

createFriendOperation = `
mutation CreateFriend($friendVariable: InputFriend!) {
createFriend(friend: $friendVariable) {
Expand Down Expand Up @@ -1019,7 +1046,7 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
BufferId: 0,
Input: `{"query_params":[{"name":"names","value":"$$0$$"}],"method":"GET","url":"https://example.com/friend"}`,
Input: `{"query_params":[{"name":"names","value":$$0$$}],"method":"GET","url":"https://example.com/friend"}`,
DataSource: &Source{},
Variables: resolve.NewVariables(
&resolve.ContextVariable{
Expand Down Expand Up @@ -1086,6 +1113,225 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
DisableResolveFieldPositions: true,
},
))
t.Run("get request with int argument query param", datasourcetesting.RunTest(schema, intArgumentOperation, "ArgumentQuery",
&plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
BufferId: 0,
Input: `{"query_params":[{"name":"limit","value":$$0$$}],"method":"GET","url":"https://example.com/friend"}`,
DataSource: &Source{},
Variables: resolve.NewVariables(
&resolve.ContextVariable{
Path: []string{"a"},
Renderer: resolve.NewPlainVariableRendererWithValidation(`{"type":["integer","null"]}`),
},
),
DataSourceIdentifier: []byte("rest_datasource.Source"),
DisableDataLoader: true,
},
Fields: []*resolve.Field{
{
BufferID: 0,
HasBuffer: true,
Name: []byte("withIntArgument"),
Value: &resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
Nullable: true,
},
},
},
},
},
},
},
},
},
plan.Configuration{
DataSources: []plan.DataSourceConfiguration{
{
RootNodes: []plan.TypeField{
{
TypeName: "Query",
FieldNames: []string{"withIntArgument"},
},
},
Custom: ConfigJSON(Configuration{
Fetch: FetchConfiguration{
URL: "https://example.com/friend",
Method: "GET",
Query: []QueryConfiguration{
{
Name: "limit",
Value: "{{ .arguments.limit }}",
},
},
},
}),
Factory: &Factory{},
},
},
Fields: []plan.FieldConfiguration{
{
TypeName: "Query",
FieldName: "withIntArgument",
DisableDefaultMapping: true,
},
},
DisableResolveFieldPositions: true,
},
))
t.Run("get request with non null int as query param", datasourcetesting.RunTest(schema, intArgumentOperationNonNullableInt, "ArgumentQuery",
&plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
BufferId: 0,
Input: `{"query_params":[{"name":"limit","value":$$0$$}],"method":"GET","url":"https://example.com/friend"}`,
DataSource: &Source{},
Variables: resolve.NewVariables(
&resolve.ContextVariable{
Path: []string{"in"},
Renderer: resolve.NewPlainVariableRendererWithValidation(`{"type":["integer"]}`),
},
),
DataSourceIdentifier: []byte("rest_datasource.Source"),
DisableDataLoader: true,
},
Fields: []*resolve.Field{
{
BufferID: 0,
HasBuffer: true,
Name: []byte("withIntArgument"),
Value: &resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
Nullable: true,
},
},
},
},
},
},
},
},
},
plan.Configuration{
DataSources: []plan.DataSourceConfiguration{
{
RootNodes: []plan.TypeField{
{
TypeName: "Query",
FieldNames: []string{"withIntArgument"},
},
},
Custom: ConfigJSON(Configuration{
Fetch: FetchConfiguration{
URL: "https://example.com/friend",
Method: "GET",
Query: []QueryConfiguration{
{
Name: "limit",
Value: "{{ .arguments.limit }}",
},
},
},
}),
Factory: &Factory{},
},
},
Fields: []plan.FieldConfiguration{
{
TypeName: "Query",
FieldName: "withIntArgument",
DisableDefaultMapping: true,
},
},
DisableResolveFieldPositions: true,
},
))
t.Run("get request with non null string as query param", datasourcetesting.RunTest(schema, stringArgumentOperationNonNullableString, "ArgumentQuery",
&plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
BufferId: 0,
Input: `{"query_params":[{"name":"name","value":"$$0$$"}],"method":"GET","url":"https://example.com/friend"}`,
DataSource: &Source{},
Variables: resolve.NewVariables(
&resolve.ContextVariable{
Path: []string{"in"},
Renderer: resolve.NewPlainVariableRendererWithValidation(`{"type":["string"]}`),
},
),
DataSourceIdentifier: []byte("rest_datasource.Source"),
DisableDataLoader: true,
},
Fields: []*resolve.Field{
{
BufferID: 0,
HasBuffer: true,
Name: []byte("withStringArgument"),
Value: &resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
Nullable: true,
},
},
},
},
},
},
},
},
},
plan.Configuration{
DataSources: []plan.DataSourceConfiguration{
{
RootNodes: []plan.TypeField{
{
TypeName: "Query",
FieldNames: []string{"withStringArgument"},
},
},
Custom: ConfigJSON(Configuration{
Fetch: FetchConfiguration{
URL: "https://example.com/friend",
Method: "GET",
Query: []QueryConfiguration{
{
Name: "name",
Value: "{{ .arguments.name }}",
},
},
},
}),
Factory: &Factory{},
},
},
Fields: []plan.FieldConfiguration{
{
TypeName: "Query",
FieldName: "withStringArgument",
DisableDefaultMapping: true,
},
},
DisableResolveFieldPositions: true,
},
))
t.Run("get request with array query", datasourcetesting.RunTest(schema, arrayArgumentOperation, "ArgumentQuery",
&plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Expand Down

0 comments on commit 19fa44a

Please sign in to comment.