Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-8526] fix issue where query params of type int and array are wrapped with quotes #355

Merged
merged 8 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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