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

should resolve with a nested lonely data source (TT-9884) #429

Merged
merged 1 commit into from
May 14, 2024
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
56 changes: 47 additions & 9 deletions pkg/engine/datasource/graphql_datasource/graphql_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,9 @@ func (p *Planner) EnterField(ref int) {
// least the federation key for the type the field lives on is required
// (and required fields are specified in the configuration).
p.handleFederation(fieldConfiguration)
p.addField(ref)
fieldNodeIndex := p.addField(ref)

upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref
upstreamFieldRef := p.nodes[fieldNodeIndex].Ref

p.addFieldArguments(upstreamFieldRef, ref, fieldConfiguration)
}
Expand Down Expand Up @@ -619,8 +619,7 @@ func (p *Planner) EnterDocument(_, _ *ast.Document) {
}
}

func (p *Planner) LeaveDocument(_, _ *ast.Document) {
}
func (p *Planner) LeaveDocument(_, _ *ast.Document) {}

func (p *Planner) handleFederation(fieldConfig *plan.FieldConfiguration) {
if !p.config.Federation.Enabled { // federation must be enabled
Expand Down Expand Up @@ -1052,6 +1051,7 @@ const (
func (p *Planner) printOperation() []byte {
buf := &bytes.Buffer{}

p.checkAndFixUpstreamOperation(p.upstreamOperation)
err := astprinter.Print(p.upstreamOperation, nil, buf)
if err != nil {
return nil
Expand Down Expand Up @@ -1136,6 +1136,20 @@ func (p *Planner) printOperation() []byte {
return buf.Bytes()
}

func (p *Planner) checkAndFixUpstreamOperation(operation *ast.Document) {
p.checkAndFixEmptySelectionSets(operation)
}

func (p *Planner) checkAndFixEmptySelectionSets(operation *ast.Document) {
for i := 0; i < len(operation.SelectionSets); i++ {
if len(operation.SelectionSets[i].SelectionRefs) > 0 {
continue
}

p.addTypenameToSelectionSet(i)
}
}

func (p *Planner) stopWithError(msg string, args ...interface{}) {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf(msg, args...))
}
Expand Down Expand Up @@ -1283,21 +1297,45 @@ func (p *Planner) handleFieldAlias(ref int) (newFieldName string, alias ast.Alia
}

// addField - add a field to an upstream operation
func (p *Planner) addField(ref int) {
func (p *Planner) addField(ref int) (addedNodeIndex int) {
fieldName, alias := p.handleFieldAlias(ref)

field := p.upstreamOperation.AddField(ast.Field{
field := ast.Field{
Name: p.upstreamOperation.Input.AppendInputString(fieldName),
Alias: alias,
})
}

fieldNode := p.upstreamOperation.AddField(field)

selection := ast.Selection{
Kind: ast.SelectionKindField,
Ref: field.Ref,
Ref: fieldNode.Ref,
}

p.upstreamOperation.AddSelection(p.nodes[len(p.nodes)-1].Ref, selection)
p.nodes = append(p.nodes, field)
p.nodes = append(p.nodes, fieldNode)
addedNodeIndex = len(p.nodes) - 1

definitions := p.visitor.Definition.NodeFieldDefinitions(p.visitor.Walker.EnclosingTypeDefinition)
for _, fieldDefRef := range definitions {
definitionFieldName := p.visitor.Definition.FieldDefinitionNameBytes(fieldDefRef)
if !bytes.Equal([]byte(fieldName), definitionFieldName) {
continue
}

fieldDefKind := p.visitor.Definition.FieldDefinitionTypeNode(fieldDefRef).Kind
if fieldDefKind == ast.NodeKindObjectTypeDefinition || fieldDefKind == ast.NodeKindInterfaceTypeDefinition {
selectionSetNode := p.upstreamOperation.AddSelectionSet()
p.nodes = append(p.nodes, selectionSetNode)

p.upstreamOperation.Fields[fieldNode.Ref].HasSelections = true
p.upstreamOperation.Fields[fieldNode.Ref].SelectionSet = selectionSetNode.Ref

break
}
}

return addedNodeIndex
}

type OnWsConnectionInitCallback func(ctx context.Context, url string, header http.Header) (json.RawMessage, error)
Expand Down
129 changes: 128 additions & 1 deletion pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5678,7 +5678,7 @@ func TestGraphQLDataSource(t *testing.T) {
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
BufferId: 0,
Input: `{"method":"POST","url":"http://user.service","body":{"query":"{me {__typename id uid: id username}}"}}`,
Input: `{"method":"POST","url":"http://user.service","body":{"query":"{me {id __typename uid: id username}}"}}`,
DataSource: &Source{},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true},
Expand Down Expand Up @@ -7750,6 +7750,127 @@ func TestGraphQLDataSource(t *testing.T) {
},
DisableResolveFieldPositions: true,
}))

t.Run("nested field mapping should work when nested field is alone in query", RunTest(countriesSchema, `
query Country($code: ID!) {
country(code: $code) {
continent {
name
}
}
}
`, "Country", &plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Data: &resolve.Object{
Fetch: &resolve.SingleFetch{
DataSource: &Source{},
BufferId: 0,
Input: `{"method":"POST","url":"http://countries.service/query","body":{"query":"query($code: ID!){country(code: $code){__typename}}","variables":{"code":$$0$$}}}`,
Variables: resolve.NewVariables(
&resolve.ContextVariable{
Path: []string{"code"},
Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":["string","integer"]}`),
},
),
DataSourceIdentifier: []byte("graphql_datasource.Source"),
ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true},
},
Fields: []*resolve.Field{
{
HasBuffer: true,
BufferID: 0,
Name: []byte("country"),
Value: &resolve.Object{
Path: []string{"country"},
Nullable: true,
Fields: []*resolve.Field{
{
HasBuffer: true,
Name: []byte("continent"),
BufferID: 1,
Value: &resolve.Object{
Path: []string{"continent"},
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
},
},
},
},
},
},
Fetch: &resolve.SingleFetch{
DataSource: &Source{},
BufferId: 1,
Input: `{"method":"POST","url":"http://continents.service/query","body":{"query":"{continent {name}}"}}`,
DataSourceIdentifier: []byte("graphql_datasource.Source"),
ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true},
},
},
},
},
},
},
}, plan.Configuration{
DataSources: []plan.DataSourceConfiguration{
{
RootNodes: []plan.TypeField{
{
TypeName: "Query",
FieldNames: []string{"country"},
},
},
ChildNodes: []plan.TypeField{
{
TypeName: "Country",
FieldNames: []string{"name", "code"},
},
},
Factory: &Factory{},
Custom: ConfigJson(Configuration{
Fetch: FetchConfiguration{
URL: "http://countries.service/query",
},
}),
},
{
RootNodes: []plan.TypeField{
{
TypeName: "Country",
FieldNames: []string{"continent"},
},
},
ChildNodes: []plan.TypeField{
{
TypeName: "Continent",
FieldNames: []string{"name", "code"},
},
},
Factory: &Factory{},
Custom: ConfigJson(Configuration{
Fetch: FetchConfiguration{
URL: "http://continents.service/query",
},
}),
},
},
Fields: []plan.FieldConfiguration{
{
TypeName: "Query",
FieldName: "country",
Path: []string{"country"},
Arguments: []plan.ArgumentConfiguration{
{
Name: "code",
SourceType: plan.FieldArgumentSource,
},
},
},
},
DisableResolveFieldPositions: true,
}))
}

var errSubscriptionClientFail = errors.New("subscription client fail error")
Expand Down Expand Up @@ -9693,6 +9814,12 @@ schema {
type Country {
name: String!
code: ID!
continent: Continent!
}

type Continent {
name: String!
code: ID!
}

type Query {
Expand Down
93 changes: 93 additions & 0 deletions pkg/graphql/execution_engine_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,97 @@ func TestExecutionEngineV2_Execute(t *testing.T) {
},
))

t.Run("execute with empty selection set of data source", runWithoutError(
ExecutionEngineV2TestCase{
schema: func(t *testing.T) *Schema {
t.Helper()
schema, err := NewSchemaFromString(`
type Query {
countries: [Country]
}

type Country {
name: String!
code: ID!
continent: Continent!
}

type Continent {
name: String!
}
`)
require.NoError(t, err)
return schema
}(t),
operation: func(t *testing.T) Request {
t.Helper()
return Request{
OperationName: "CountriesContinent",
Query: `
query CountriesContinent {
countries {
continent {
name
}
}
}
`,
}
},
dataSources: []plan.DataSourceConfiguration{
{
RootNodes: []plan.TypeField{
{TypeName: "Query", FieldNames: []string{"countries"}},
},
Factory: &graphql_datasource.Factory{
HTTPClient: testNetHttpClient(t, roundTripperTestCase{
expectedHost: "countries.example.com",
expectedPath: "/graphql",
expectedBody: `{"query":"{countries {__typename}}"}`,
sendResponseBody: `{"data": {"countries": {"__typename": "Country"}}}`,
sendStatusCode: 200,
}),
},
Custom: graphql_datasource.ConfigJson(graphql_datasource.Configuration{
Fetch: graphql_datasource.FetchConfiguration{
URL: "https://countries.example.com/graphql",
Method: "POST",
},
}),
},
{
RootNodes: []plan.TypeField{
{TypeName: "Country", FieldNames: []string{"continent"}},
},
Factory: &rest_datasource.Factory{
Client: testNetHttpClient(t, roundTripperTestCase{
expectedHost: "continents.example.com",
expectedPath: "/1",
expectedBody: "",
sendResponseBody: `{"name": "Europe"}`,
sendStatusCode: 200,
}),
},
Custom: rest_datasource.ConfigJSON(rest_datasource.Configuration{
Fetch: rest_datasource.FetchConfiguration{
URL: "https://continents.example.com/1",
Method: "GET",
},
}),
},
},
fields: []plan.FieldConfiguration{
{
TypeName: "Country",
FieldName: "continent",
DisableDefaultMapping: true,
Path: []string{""},
},
},
expectedResponse: `{"data":{"countries":[{"continent":{"name":"Europe"}}]}}`,
},
))

t.Run("execute with .object and .arguments placeholder", runWithoutError(
ExecutionEngineV2TestCase{
schema: func(t *testing.T) *Schema {
Expand Down Expand Up @@ -2108,6 +2199,8 @@ func testNetHttpClient(t *testing.T, testCase roundTripperTestCase) *http.Client
}
}

type hostBasedRoundTripperTestCases map[string]roundTripperTestCase

type beforeFetchHook struct {
input string
}
Expand Down
Loading