diff --git a/pkg/engine/datasource/rest_datasource/rest_datasource.go b/pkg/engine/datasource/rest_datasource/rest_datasource.go index 6c88688102..5b9caafcef 100644 --- a/pkg/engine/datasource/rest_datasource/rest_datasource.go +++ b/pkg/engine/datasource/rest_datasource/rest_datasource.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "encoding/json" + "fmt" + "github.com/buger/jsonparser" "io" "net/http" "regexp" @@ -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 @@ -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 { @@ -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) } @@ -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 { @@ -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 @@ -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 } diff --git a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go index 9301df876f..845ccb6bdc 100644 --- a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go +++ b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go @@ -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 { @@ -45,6 +47,7 @@ const ( name: String pet: Pet phone(name: String!): String + hasArg(limit: Int!): String } type Pet { @@ -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) { @@ -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{ @@ -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{