diff --git a/router-tests/go.mod b/router-tests/go.mod index b697d39a89..42d008a28f 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -26,7 +26,7 @@ require ( github.com/wundergraph/cosmo/demo v0.0.0-20250729121718-5f0a0b8b1804 github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e github.com/wundergraph/cosmo/router v0.0.0-20250729121718-5f0a0b8b1804 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index d8e74dc7b9..8c94a6d270 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -325,8 +325,8 @@ github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTB github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301 h1:EzfKHQoTjFDDcgaECCCR2aTePqMu9QBmPbyhqIYOhV0= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301/go.mod h1:wxI0Nak5dI5RvJuzGyiEK4nZj0O9X+Aw6U0tC1wPKq0= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213 h1:sSj/U/wgNlze02M3rVeZLY9lR1RDTRui02zUdEdfxWI= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213/go.mod h1:DnYY1alnsgzkanSwbFiFIdXKOuf8dHQWQ2P4BzTc6aI= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215 h1:ViwIW4zc/QM1yGNQQ/jTNjcc5a5mKEYZMaf6IEUd2L4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215/go.mod h1:DnYY1alnsgzkanSwbFiFIdXKOuf8dHQWQ2P4BzTc6aI= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/router-tests/integration_test.go b/router-tests/integration_test.go index 66201d696f..7a922b8a8a 100644 --- a/router-tests/integration_test.go +++ b/router-tests/integration_test.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "sync" @@ -587,6 +588,130 @@ func TestVariables(t *testing.T) { }) } +func TestPropagateOperationName(t *testing.T) { + t.Parallel() + + t.Run("simple", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) { + cfg.EnableSubgraphFetchOperationName = true + }, + Subgraphs: testenv.SubgraphsConfig{ + Employees: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req core.GraphQLRequest + require.NoError(t, json.Unmarshal(body, &req)) + + require.Equal(t, `query ScalarRequest__employees__0($a: Int!){employee(id: $a){id}}`, req.Query) + require.Equal(t, json.RawMessage(`{"a":1}`), req.Variables) + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"data":{"employee":{"id":1}}}`)) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + t.Run("scalar argument type", func(t *testing.T) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query ScalarRequest ($count:Int!) { employee(id:$count) { id } }`, + Variables: json.RawMessage(`{"count":1}`), + }) + require.JSONEq(t, `{"data":{"employee":{"id":1}}}`, res.Body) + }) + }) + }) + + t.Run("complex", func(t *testing.T) { + t.Parallel() + employeePrefix := "query Requires__employees__" + var mut sync.Mutex + // Middleware for Employee Subgraph should remove queries it sees. + expectEmployeeOps := []string{employeePrefix + "0", employeePrefix + "3", employeePrefix + "4"} + + testenv.Run(t, &testenv.Config{ + ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) { + cfg.EnableSubgraphFetchOperationName = true + }, + Subgraphs: testenv.SubgraphsConfig{ + Employees: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req core.GraphQLRequest + require.NoError(t, json.Unmarshal(body, &req)) + + got := req.Query[:len(employeePrefix)+1] + mut.Lock() + idx := slices.Index(expectEmployeeOps, got) + require.True(t, idx != -1, "expected one of %v, got %v", expectEmployeeOps, got) + expectEmployeeOps = slices.Delete(expectEmployeeOps, idx, idx+1) + mut.Unlock() + + r.Body = io.NopCloser(bytes.NewReader(body)) + handler.ServeHTTP(w, r) + }) + }, + }, + Mood: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req core.GraphQLRequest + require.NoError(t, json.Unmarshal(body, &req)) + + require.Contains(t, req.Query, "query Requires__mood__1") + + r.Body = io.NopCloser(bytes.NewReader(body)) + handler.ServeHTTP(w, r) + }) + }, + }, + Availability: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req core.GraphQLRequest + require.NoError(t, json.Unmarshal(body, &req)) + + require.Contains(t, req.Query, "query Requires__availability__2") + + r.Body = io.NopCloser(bytes.NewReader(body)) + handler.ServeHTTP(w, r) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + t.Run("scalar argument type", func(t *testing.T) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query Requires { + products { + __typename + ... on Consultancy { + lead { + __typename + id + derivedMood + } + isLeadAvailable + } + } + }`, + }) + require.Empty(t, expectEmployeeOps, "unexpected remaining operations") + require.JSONEq(t, `{"data":{"products":[{"__typename":"Consultancy","lead":{"__typename":"Employee","id":1,"derivedMood":"HAPPY"},"isLeadAvailable":false},{"__typename":"Cosmo"},{"__typename":"SDK"}]}}`, res.Body) + }) + }) + }) +} + func TestVariablesRemapping(t *testing.T) { t.Parallel() diff --git a/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name.json b/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name.json index cb43ca09e6..4f3aa3637f 100644 --- a/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name.json +++ b/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name.json @@ -30,7 +30,7 @@ "subgraphName": "employees", "subgraphId": "0", "fetchId": 0, - "query": "query Requires__employees {\n products {\n __typename\n ... on Consultancy {\n lead {\n __typename\n id\n }\n __typename\n upc\n }\n }\n}" + "query": "query Requires__employees__0 {\n products {\n __typename\n ... on Consultancy {\n lead {\n __typename\n id\n }\n __typename\n upc\n }\n }\n}" } }, { @@ -54,7 +54,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__mood($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n currentMood\n }\n }\n}", + "query": "query Requires__mood__1($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n currentMood\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -96,7 +96,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__availability($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n isAvailable\n }\n }\n}", + "query": "query Requires__availability__2($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n isAvailable\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -150,7 +150,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__employees($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n derivedMood\n }\n }\n}", + "query": "query Requires__employees__3($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n derivedMood\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -209,7 +209,7 @@ "fragment": "fragment Key on Consultancy {\n __typename\n upc\n}" } ], - "query": "query Requires__employees($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Consultancy {\n __typename\n isLeadAvailable\n }\n }\n}", + "query": "query Requires__employees__4($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Consultancy {\n __typename\n isLeadAvailable\n }\n }\n}", "dependencies": [ { "coordinate": { diff --git a/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name_sanitized_no_data.json b/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name_sanitized_no_data.json index 29ab8afe5c..3a3df54648 100644 --- a/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name_sanitized_no_data.json +++ b/router-tests/testdata/fixtures/query_plans/response_with_query_plan_operation_name_sanitized_no_data.json @@ -12,7 +12,7 @@ "subgraphName": "employees", "subgraphId": "0", "fetchId": 0, - "query": "query Requires__employees {\n products {\n __typename\n ... on Consultancy {\n lead {\n __typename\n id\n }\n __typename\n upc\n }\n }\n}" + "query": "query Requires__employees__0 {\n products {\n __typename\n ... on Consultancy {\n lead {\n __typename\n id\n }\n __typename\n upc\n }\n }\n}" } }, { @@ -36,7 +36,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__mo_o_d($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n currentMood\n }\n }\n}", + "query": "query Requires__mo_o_d__1($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n currentMood\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -78,7 +78,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__av_ai_la_bi_lit_y($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n isAvailable\n }\n }\n}", + "query": "query Requires__av_ai_la_bi_lit_y__2($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n isAvailable\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -132,7 +132,7 @@ "fragment": "fragment Key on Employee {\n __typename\n id\n}" } ], - "query": "query Requires__employees($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n derivedMood\n }\n }\n}", + "query": "query Requires__employees__3($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Employee {\n __typename\n derivedMood\n }\n }\n}", "dependencies": [ { "coordinate": { @@ -191,7 +191,7 @@ "fragment": "fragment Key on Consultancy {\n __typename\n upc\n}" } ], - "query": "query Requires__employees($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Consultancy {\n __typename\n isLeadAvailable\n }\n }\n}", + "query": "query Requires__employees__4($representations: [_Any!]!){\n _entities(representations: $representations){\n ... on Consultancy {\n __typename\n isLeadAvailable\n }\n }\n}", "dependencies": [ { "coordinate": { diff --git a/router/go.mod b/router/go.mod index f2ff341687..a89fb2efde 100644 --- a/router/go.mod +++ b/router/go.mod @@ -31,7 +31,7 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/twmb/franz-go v1.16.1 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index 1c835c25af..f3f9ae8c98 100644 --- a/router/go.sum +++ b/router/go.sum @@ -290,8 +290,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213 h1:sSj/U/wgNlze02M3rVeZLY9lR1RDTRui02zUdEdfxWI= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.213/go.mod h1:DnYY1alnsgzkanSwbFiFIdXKOuf8dHQWQ2P4BzTc6aI= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215 h1:ViwIW4zc/QM1yGNQQ/jTNjcc5a5mKEYZMaf6IEUd2L4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.215/go.mod h1:DnYY1alnsgzkanSwbFiFIdXKOuf8dHQWQ2P4BzTc6aI= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 048376349a..973c65eab9 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -2723,7 +2723,7 @@ "enable_subgraph_fetch_operation_name": { "type": "boolean", "default": false, - "description": "Enable appending the operation name to subgraph fetches. This will ensure that the operation name will be included in the corresponding subgraph requests using the following format: $operationName__$subgraphName__$sequenceID." + "description": "Enable appending the operation name to subgraph fetches. This will ensure that the operation name will be included in the corresponding subgraph requests using the following format: $OperationName__$SubgraphName__$FetchID." }, "disable_variables_remapping": { "type": "boolean",