Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/wundergraph/graphql-go-tools/v2
go 1.25

require (
connectrpc.com/connect v1.19.2
github.com/99designs/gqlgen v0.17.76
github.com/bufbuild/protocompile v0.14.1
github.com/buger/jsonparser v1.1.1
Expand Down Expand Up @@ -31,6 +32,7 @@ require (
github.com/wundergraph/astjson v1.1.0
github.com/wundergraph/go-arena v1.1.0
go.uber.org/goleak v1.3.0
golang.org/x/net v0.46.0
golang.org/x/sync v0.17.0
golang.org/x/sys v0.37.0
golang.org/x/text v0.30.0
Expand Down Expand Up @@ -74,7 +76,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
Expand Down
4 changes: 3 additions & 1 deletion v2/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
github.com/99designs/gqlgen v0.17.76 h1:YsJBcfACWmXWU2t1yCjoGdOmqcTfOFpjbLAE443fmYI=
github.com/99designs/gqlgen v0.17.76/go.mod h1:miiU+PkAnTIDKMQ1BseUOIVeQHoiwYDZGCswoxl7xec=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down Expand Up @@ -27,11 +29,11 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7c
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package grpcdatasource

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"

"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest"
"github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest/productv1/productv1connect"
)

// setupTestConnectServer starts an httptest server backed by the
// MockServiceConnect adapter (the gRPC MockService wrapped onto the
// ConnectRPC handler interface). The server speaks Connect, gRPC, and
// gRPC-Web on the same H2C endpoint, but for these tests we drive it via
// the Connect transport.
//
// Returns a base URL that can be passed to NewConnectTransport.
func setupTestConnectServer(t testing.TB) (baseURL string, cleanup func()) {
t.Helper()

mock := &grpctest.MockService{}
connectImpl := grpctest.NewMockServiceConnect(mock)

mux := http.NewServeMux()
mux.Handle(productv1connect.NewProductServiceHandler(connectImpl))

srv := httptest.NewUnstartedServer(h2c.NewHandler(mux, &http2.Server{}))
srv.EnableHTTP2 = true
srv.Start()

cleanup = srv.Close
return srv.URL, cleanup
}

// Test_DataSource_Load_WithMockServiceConnect mirrors the gRPC end-to-end
// happy path (Test_DataSource_Load_WithMockService) but routes the call
// through the Connect transport instead of the gRPC client connection.
// It proves that the data source pipeline (compiler -> JSON builder ->
// transport -> response unmarshal) works for the Connect protocol against
// the same MockService implementation.
func Test_DataSource_Load_WithMockServiceConnect(t *testing.T) {
baseURL, cleanup := setupTestConnectServer(t)
t.Cleanup(cleanup)

query := `query ComplexFilterTypeQuery($filter: ComplexFilterTypeInput!) { complexFilterType(filter: $filter) { id name } }`
variables := `{"variables":{"filter":{"filter":{"name":"Test Product","filterField1":"filterField1","filterField2":"filterField2"}}}}`

schemaDoc := grpctest.MustGraphQLSchema(t)
queryDoc, report := astparser.ParseGraphqlDocumentString(query)
if report.HasErrors() {
t.Fatalf("failed to parse query: %s", report.Error())
}

compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), nil)
require.NoError(t, err)

transport := NewConnectTransport(ConnectTransportConfig{
BaseURL: baseURL,
Encoding: ConnectEncodingProtobuf,
})

ds, err := NewDataSource(transport, DataSourceConfig{
Operation: &queryDoc,
Definition: &schemaDoc,
SubgraphName: "Products",
Compiler: compiler,
Mapping: &GRPCMapping{
Service: "Products",
QueryRPCs: RPCConfigMap[RPCConfig]{
"complexFilterType": {
RPC: "QueryComplexFilterType",
Request: "QueryComplexFilterTypeRequest",
Response: "QueryComplexFilterTypeResponse",
},
},
Fields: map[string]FieldMap{
"Query": {
"complexFilterType": {
TargetName: "complex_filter_type",
ArgumentMappings: map[string]string{
"filter": "filter",
},
},
},
"FilterType": {
"name": {TargetName: "name"},
"filterField1": {TargetName: "filter_field_1"},
"filterField2": {TargetName: "filter_field_2"},
},
},
},
})
require.NoError(t, err)

output, err := ds.Load(context.Background(), nil, []byte(`{"query":"`+query+`","body":`+variables+`}`))
require.NoError(t, err)

type response struct {
Data struct {
ComplexFilterType []struct {
Id string `json:"id"`
Name string `json:"name"`
} `json:"complexFilterType"`
} `json:"data"`
}
var resp response
require.NoError(t, json.Unmarshal(output, &resp))
require.Equal(t, "test-id-123", resp.Data.ComplexFilterType[0].Id)
require.Equal(t, "Test Product", resp.Data.ComplexFilterType[0].Name)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Test_DataSource_Load_WithMockServiceConnect_JSON re-runs the same
// happy-path query with JSON encoding instead of Protobuf. Both wire
// formats must yield identical decoded responses.
func Test_DataSource_Load_WithMockServiceConnect_JSON(t *testing.T) {
baseURL, cleanup := setupTestConnectServer(t)
t.Cleanup(cleanup)

query := `query ComplexFilterTypeQuery($filter: ComplexFilterTypeInput!) { complexFilterType(filter: $filter) { id name } }`
variables := `{"variables":{"filter":{"filter":{"name":"Test Product","filterField1":"a","filterField2":"b"}}}}`

schemaDoc := grpctest.MustGraphQLSchema(t)
queryDoc, report := astparser.ParseGraphqlDocumentString(query)
if report.HasErrors() {
t.Fatalf("failed to parse query: %s", report.Error())
}

compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), nil)
require.NoError(t, err)

transport := NewConnectTransport(ConnectTransportConfig{
BaseURL: baseURL,
Encoding: ConnectEncodingJSON,
})

ds, err := NewDataSource(transport, DataSourceConfig{
Operation: &queryDoc,
Definition: &schemaDoc,
SubgraphName: "Products",
Compiler: compiler,
Mapping: &GRPCMapping{
Service: "Products",
QueryRPCs: RPCConfigMap[RPCConfig]{
"complexFilterType": {
RPC: "QueryComplexFilterType",
Request: "QueryComplexFilterTypeRequest",
Response: "QueryComplexFilterTypeResponse",
},
},
Fields: map[string]FieldMap{
"Query": {
"complexFilterType": {
TargetName: "complex_filter_type",
ArgumentMappings: map[string]string{
"filter": "filter",
},
},
},
"FilterType": {
"name": {TargetName: "name"},
"filterField1": {TargetName: "filter_field_1"},
"filterField2": {TargetName: "filter_field_2"},
},
},
},
})
require.NoError(t, err)

output, err := ds.Load(context.Background(), nil, []byte(`{"query":"`+query+`","body":`+variables+`}`))
require.NoError(t, err)

require.Contains(t, string(output), `"id":"test-id-123"`)
require.Contains(t, string(output), `"name":"Test Product"`)
}
11 changes: 10 additions & 1 deletion v2/pkg/engine/datasource/grpc_datasource/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import (
)

// RPCTransport abstracts the transport protocol for RPC calls.
// Both gRPC and Connect protocol implement this interface.
// Both gRPC and Connect implement this interface.
//
// Invoke dispatches a unary call against the remote service.
// - methodFullName is the gRPC-style procedure path "/package.Service/Method"
// (leading slash required). The gRPC transport passes it directly to
// grpc.ClientConnInterface.Invoke; the Connect transport appends it to
// the configured base URL.
// - input must be a *dynamicpb.Message populated by the caller.
// - output must be a *dynamicpb.Message bound to the expected response
// descriptor; Invoke populates it on success.
type RPCTransport interface {
Invoke(ctx context.Context, methodFullName string, input, output protoref.Message) error
}
Expand Down
Loading