Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d1fdf79
feat: add initial planner support for @requires
Noroth Jan 13, 2026
e612849
feat: add support for calling requires rpcs
Noroth Jan 19, 2026
5aeac1f
chore: remove commented function
Noroth Jan 19, 2026
f9a48d6
chore: improve code
Noroth Jan 19, 2026
93dfcd5
Merge branch 'master' of github.com:wundergraph/graphql-go-tools into…
Noroth Jan 27, 2026
e30ed83
chore: add additional fields to the schema. Cleanup tests in grpcdata…
Noroth Jan 27, 2026
62cc7fe
chore: use Storage type instead
Noroth Jan 27, 2026
e73e253
chore: add more tests for required fields
Noroth Jan 27, 2026
84635dc
add tests to the datasource
Noroth Jan 28, 2026
22fc16f
chore: make required fields ordered
Noroth Jan 29, 2026
ab8d568
chore: use underlying prototype for mapping the data type
Noroth Jan 29, 2026
5ce2bd9
chore: add tests for field resolvers for the datasource
Noroth Jan 30, 2026
77febb8
chore: run linter fixes
Noroth Jan 30, 2026
19d5847
Merge branch 'master' into ludwig/eng-8642-add-support-for-plain-fields
Noroth Jan 30, 2026
925ca50
chore: add compile time assertions
Noroth Jan 30, 2026
3190ea2
chore: pr review comments
Noroth Jan 30, 2026
98b0f3d
Merge branch 'master' into ludwig/eng-8642-add-support-for-plain-fields
Noroth Feb 6, 2026
f4d39d3
Merge branch 'master' of github.com:wundergraph/graphql-go-tools into…
Noroth Feb 23, 2026
5ca07fd
Merge branch 'master' of github.com:wundergraph/graphql-go-tools into…
Noroth Feb 23, 2026
8542e9d
chore: cleanup
Noroth Feb 23, 2026
a413cc0
chore: fix json in test
Noroth Feb 23, 2026
a33eef6
Merge branch 'master' into ludwig/eng-8642-add-support-for-plain-fields
Noroth Feb 27, 2026
5a2861c
chore: fix benchmark
Noroth Feb 27, 2026
cf56ec2
chore: update proto
Noroth Feb 27, 2026
e30c9ee
Merge branch 'master' into ludwig/eng-8642-add-support-for-plain-fields
Noroth Mar 4, 2026
0534f41
chore: update from PR comments
Noroth Mar 4, 2026
88166f1
Merge branch 'master' of github.com:wundergraph/graphql-go-tools into…
Noroth Mar 9, 2026
b57eceb
chore: fix linting
Noroth Mar 9, 2026
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
4 changes: 4 additions & 0 deletions execution/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ format:
lint:
golangci-lint run

.PHONY: lint-fix
lint-fix:
golangci-lint run --fix

.PHONY: prepare-merge
prepare-merge: format test

Expand Down
5 changes: 5 additions & 0 deletions v2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ lint:
golangci-lint run
cd ../execution && make lint

.PHONY: lint-fix
lint-fix:
golangci-lint run --fix
cd ../execution && make lint-fix

.PHONY: prepare-merge
prepare-merge: format test

Expand Down
8 changes: 4 additions & 4 deletions v2/pkg/astvisitor/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4043,15 +4043,15 @@ func (w *Walker) InRootField() bool {
// an inline fragment.
// It returns the inline fragment ref and true if the current field is inside of an inline fragment.
// It returns -1 and false if the current field is not inside of an inline fragment.
func (w *Walker) ResolveInlineFragment() (int, bool) {
func (w *Walker) ResolveInlineFragment() int {
if len(w.Ancestors) < 2 {
return ast.InvalidRef, false
return ast.InvalidRef
}

node := w.Ancestors[len(w.Ancestors)-2]
if node.Kind != ast.NodeKindInlineFragment {
return ast.InvalidRef, false
return ast.InvalidRef
}

return node.Ref, true
return node.Ref
}
206 changes: 138 additions & 68 deletions v2/pkg/engine/datasource/grpc_datasource/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,53 +21,8 @@ const (
InvalidRef = -1
)

// DataType represents the different types of data that can be stored in a protobuf field.
type DataType string

// Protobuf data types supported by the compiler.
const (
DataTypeString DataType = "string" // String type
DataTypeInt32 DataType = "int32" // 32-bit integer type
DataTypeInt64 DataType = "int64" // 64-bit integer type
DataTypeUint32 DataType = "uint32" // 32-bit unsigned integer type
DataTypeUint64 DataType = "uint64" // 64-bit unsigned integer type
DataTypeFloat DataType = "float" // 32-bit floating point type
DataTypeDouble DataType = "double" // 64-bit floating point type
DataTypeBool DataType = "bool" // Boolean type
DataTypeEnum DataType = "enum" // Enumeration type
DataTypeMessage DataType = "message" // Nested message type
DataTypeUnknown DataType = "<unknown>" // Represents an unknown or unsupported type
DataTypeBytes DataType = "bytes" // Bytes type
)

// dataTypeMap maps string representation of protobuf types to DataType constants.
var dataTypeMap = map[string]DataType{
"string": DataTypeString,
"int32": DataTypeInt32,
"int64": DataTypeInt64,
"uint32": DataTypeUint32,
"uint64": DataTypeUint64,
"float": DataTypeFloat,
"double": DataTypeDouble,
"bool": DataTypeBool,
"bytes": DataTypeBytes,
"enum": DataTypeEnum,
"message": DataTypeMessage,
}

// String returns the string representation of the DataType.
func (d DataType) String() string {
return string(d)
}

// IsValid checks if the DataType is a valid protobuf type.
func (d DataType) IsValid() bool {
_, ok := dataTypeMap[string(d)]
return ok
}

func fromGraphQLType(s string) DataType {
switch s {
func fromGraphQLType(s []byte) DataType {
switch string(s) {
case "ID", "String":
return DataTypeString
case "Int":
Expand All @@ -88,12 +43,12 @@ func fromGraphQLType(s string) DataType {

// parseDataType converts a string type name to a DataType constant.
// Returns DataTypeUnknown if the type is not recognized.
func parseDataType(name string) DataType {
if !dataTypeMap[name].IsValid() {
func parseDataType(kind protoref.Kind) DataType {
if _, ok := dataTypeMap[kind]; !ok {
return DataTypeUnknown
}

return dataTypeMap[name]
return dataTypeMap[kind]
}

type NodeKind int
Expand Down Expand Up @@ -480,24 +435,20 @@ func (p *RPCCompiler) CompileFetches(graph *DependencyGraph, fetches []FetchItem
func (p *RPCCompiler) CompileNode(graph *DependencyGraph, fetch FetchItem, inputData gjson.Result) (ServiceCall, error) {
call := fetch.Plan

inputMessage, ok := p.doc.MessageByName(call.Request.Name)
if !ok {
return ServiceCall{}, fmt.Errorf("input message %s not found in document", call.Request.Name)
}

outputMessage, ok := p.doc.MessageByName(call.Response.Name)
if !ok {
return ServiceCall{}, fmt.Errorf("output message %s not found in document", call.Response.Name)
}

request, err := p.newEmptyMessage(inputMessage)
response, err := p.newEmptyMessage(outputMessage)
if err != nil {
return ServiceCall{}, err
}

response, err := p.newEmptyMessage(outputMessage)
if err != nil {
return ServiceCall{}, err
var request protoref.Message
inputMessage, ok := p.doc.MessageByName(call.Request.Name)
if !ok {
return ServiceCall{}, fmt.Errorf("input message %s not found in document", call.Request.Name)
}

switch call.Kind {
Expand All @@ -519,6 +470,12 @@ func (p *RPCCompiler) CompileNode(graph *DependencyGraph, fetch FetchItem, input
Skip: true,
}, err
}

case CallKindRequired:
request, err = p.buildRequiredFieldsMessage(inputMessage, &call.Request, inputData)
if err != nil {
return ServiceCall{}, err
}
}

serviceName, ok := p.resolveServiceName(call)
Expand Down Expand Up @@ -653,6 +610,109 @@ func (p *RPCCompiler) buildProtoMessageWithContext(inputMessage Message, rpcMess
return rootMessage, nil
}

// buildRequiredFieldsMessage builds a protobuf message from an RPCMessage definition
// and JSON data. It handles nested messages and repeated fields.
//
// Example:
//
// message RequireWarehouseStockHealthScoreByIdRequest {
// // RequireWarehouseStockHealthScoreByIdContext provides the context for the required fields method RequireWarehouseStockHealthScoreById.
// repeated RequireWarehouseStockHealthScoreByIdContext context = 1;
// }
//
// message RequireWarehouseStockHealthScoreByIdContext {
// LookupWarehouseByIdRequestKey key = 1;
// RequireWarehouseStockHealthScoreByIdFields fields = 2;
// }
func (p *RPCCompiler) buildRequiredFieldsMessage(inputMessage Message, rpcMessage *RPCMessage, data gjson.Result) (protoref.Message, error) {
if rpcMessage == nil {
return nil, fmt.Errorf("rpc message is nil")
}

if p.doc.MessageRefByName(rpcMessage.Name) == InvalidRef {
return nil, fmt.Errorf("message %s not found in document", rpcMessage.Name)
}

rootMessage := dynamicpb.NewMessage(inputMessage.Desc)

contextSchemaField := inputMessage.GetField("context")
if contextSchemaField == nil {
return nil, fmt.Errorf("context field not found in message %s", inputMessage.Name)
}

contextRPCField := rpcMessage.Fields.ByName(contextSchemaField.Name)
if contextRPCField == nil {
return nil, fmt.Errorf("context field not found in message %s", rpcMessage.Name)
}

contextList := p.newEmptyListMessageByName(rootMessage, contextSchemaField.Name)
contextFieldMessage := contextRPCField.Message

if contextFieldMessage == nil {
return nil, fmt.Errorf("context field message not found in message %s", inputMessage.Name)
}

keyField := contextFieldMessage.Fields.ByName("key")
if keyField == nil {
return nil, fmt.Errorf("key field message not found in message %s", contextFieldMessage.Name)
}

keyMessage, ok := p.doc.MessageByName(keyField.Message.Name)
if !ok {
return nil, fmt.Errorf("message %s not found in document", keyField.Message.Name)
}

requiresSelectionField := contextFieldMessage.Fields.ByName("fields")
if requiresSelectionField == nil {
return nil, fmt.Errorf("fields field not found in message %s", contextFieldMessage.Name)
}

requiresSelectionMessage, ok := p.doc.MessageByName(requiresSelectionField.Message.Name)
if !ok {
return nil, fmt.Errorf("message %s not found in document", requiresSelectionField.Message.Name)
}

representationsValue := data.Get("representations")
if exists, isArray := representationsValue.Exists(), representationsValue.IsArray(); !exists || !isArray {
if !exists {
return nil, errors.New("representations field not found in data")
}

if !isArray {
return nil, errors.New("invalid type for representations element, expected representations to be an array")
}
}

representations := representationsValue.Array()
for _, representation := range representations {
element := contextList.NewElement()
msg := element.Message()

keyMsg, err := p.buildProtoMessage(keyMessage, keyField.Message, representation)
if err != nil {
return nil, err
}

reqMsg, err := p.buildProtoMessage(requiresSelectionMessage, requiresSelectionField.Message, representation)
if err != nil {
return nil, err
}

if err := p.setMessageValue(msg, keyField.Name, protoref.ValueOfMessage(keyMsg)); err != nil {
return nil, err
}

if err := p.setMessageValue(msg, requiresSelectionField.Name, protoref.ValueOfMessage(reqMsg)); err != nil {
return nil, err
}

// build fields message
contextList.Append(element)
}

return rootMessage, nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (p *RPCCompiler) resolveContextData(context FetchItem, contextField *RPCField) []map[string]protoref.Value {
if context.ServiceCall == nil || context.ServiceCall.Output == nil {
return []map[string]protoref.Value{}
Expand Down Expand Up @@ -734,8 +794,8 @@ func (p *RPCCompiler) resolveListDataForPath(message protoref.List, fd protoref.
}

// resolveDataForPath resolves the data for a given path in a message.
func (p *RPCCompiler) resolveDataForPath(messsage protoref.Message, path ast.Path) []protoref.Value {
if !messsage.IsValid() {
func (p *RPCCompiler) resolveDataForPath(message protoref.Message, path ast.Path) []protoref.Value {
if !message.IsValid() {
return nil
}

Expand All @@ -746,7 +806,7 @@ func (p *RPCCompiler) resolveDataForPath(messsage protoref.Message, path ast.Pat
segment := path[0]

if fn := segment.FieldName.String(); strings.HasPrefix(fn, "@") {
list := p.resolveUnderlyingList(messsage, fn)
list := p.resolveUnderlyingList(message, fn)

result := make([]protoref.Value, 0, len(list))
for _, item := range list {
Expand All @@ -756,7 +816,7 @@ func (p *RPCCompiler) resolveDataForPath(messsage protoref.Message, path ast.Pat
return result
}

field, fd := p.getMessageField(messsage, segment.FieldName.String())
field, fd := p.getMessageField(message, segment.FieldName.String())
if !field.IsValid() {
return nil
}
Expand Down Expand Up @@ -1136,7 +1196,7 @@ func (p *RPCCompiler) traverseList(rootMsg protoref.Message, level int, field *F
}
default:
for _, element := range elements {
itemsField.Append(p.setValueForKind(DataType(itemsFieldDesc.Kind().String()), element))
itemsField.Append(p.setValueForKind(parseDataType(itemsFieldDesc.Kind()), element))
}
}

Expand Down Expand Up @@ -1285,16 +1345,27 @@ func (p *RPCCompiler) parseMessageDefinitions(messages protoref.MessageDescripto
protoMessage := messages.Get(i)

message := Message{
Name: string(protoMessage.Name()),
Name: p.fullMessageName(protoMessage),
Desc: protoMessage,
}

extractedMessages = append(extractedMessages, message)

if submessages := protoMessage.Messages(); submessages.Len() > 0 {
extractedMessages = append(extractedMessages, p.parseMessageDefinitions(submessages)...)
}

}

return extractedMessages
}

// fullMessageName returns the full name of the message omiting the package name.
// In our case don't need the fqn as we only have one package where we need to resolve the messages.
func (p *RPCCompiler) fullMessageName(m protoref.MessageDescriptor) string {
return strings.TrimPrefix(string(m.FullName()), p.doc.Package+".")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// enrichMessageData enriches the message data with the field information.
func (p *RPCCompiler) enrichMessageData(ref int, m protoref.MessageDescriptor) {
fields := make([]Field, m.Fields().Len())
Expand All @@ -1307,7 +1378,7 @@ func (p *RPCCompiler) enrichMessageData(ref int, m protoref.MessageDescriptor) {

if f.Kind() == protoref.MessageKind {
// Handle nested messages when they are recursive types
field.MessageRef = p.doc.MessageRefByName(string(f.Message().Name()))
field.MessageRef = p.doc.MessageRefByName(p.fullMessageName(f.Message()))
}

fields[i] = field
Expand All @@ -1323,11 +1394,10 @@ func (p *RPCCompiler) enrichMessageData(ref int, m protoref.MessageDescriptor) {
// parseField extracts information from a protobuf field descriptor.
func (p *RPCCompiler) parseField(f protoref.FieldDescriptor) Field {
name := string(f.Name())
typeName := f.Kind().String()

return Field{
Name: name,
Type: parseDataType(typeName),
Type: parseDataType(f.Kind()),
Number: int32(f.Number()),
Repeated: f.IsList(),
Optional: f.Cardinality() == protoref.Optional,
Expand Down
Loading