Skip to content
Open
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
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,183 @@
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.NotEmpty(t, resp.Data.ComplexFilterType, "response should contain at least one item; empty slice would otherwise panic on index below")
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