From 789818f21bcfec9a6e451847005f6bad054cd54e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 30 Jun 2025 14:38:36 +0200 Subject: [PATCH 01/13] feat: add interface support --- .../grpc_datasource/execution_plan.go | 2 + .../grpc_datasource/execution_plan_test.go | 4 + .../grpc_datasource/execution_plan_visitor.go | 17 +- .../grpc_datasource/grpc_datasource.go | 44 ++- .../grpc_datasource/grpc_datasource_test.go | 338 +++++++++--------- v2/pkg/grpctest/product.proto | 112 +++--- v2/pkg/grpctest/productv1/product.pb.go | 106 +++--- 7 files changed, 349 insertions(+), 274 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index c4d99b7a5d..c15d361fc6 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -48,6 +48,8 @@ type RPCMessage struct { Fields RPCFields // OneOf indicates if the message is an interface OneOf bool + // ImplementedBy provides the names of the types that are implemented by the Interface or Union + ImplementedBy []string } // RPCField represents a single field in a gRPC message. diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index 33fae6be10..9f6e2d463a 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -1516,6 +1516,10 @@ func TestInterfaceExecutionPlan(t *testing.T) { Message: &RPCMessage{ Name: "Animal", OneOf: true, + ImplementedBy: []string{ + "Cat", + "Dog", + }, Fields: []RPCField{ { Name: "id", diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go index 100fb5ab28..a5528b267b 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go @@ -158,7 +158,10 @@ func (r *rpcPlanVisitor) EnterSelectionSet(ref int) { r.planInfo.responseMessageAncestors = append(r.planInfo.responseMessageAncestors, r.planInfo.currentResponseMessage) r.planInfo.currentResponseMessage = r.planInfo.currentResponseMessage.Fields[r.planInfo.currentResponseFieldIndex].Message - r.planInfo.currentResponseMessage.OneOf = r.isInterface(r.walker.Ancestor()) + if isInterface, interfaceRef := r.isInterface(r.walker.Ancestor()); isInterface { + r.planInfo.currentResponseMessage.OneOf = true + r.planInfo.currentResponseMessage.ImplementedBy, _ = r.definition.InterfaceTypeDefinitionImplementedByObjectWithNames(interfaceRef) + } // Keep track of the field indices for the current response message. // This is used to set the correct field index for the current response message @@ -168,23 +171,27 @@ func (r *rpcPlanVisitor) EnterSelectionSet(ref int) { r.planInfo.currentResponseFieldIndex = 0 // reset the field index for the current selection set } -func (r *rpcPlanVisitor) isInterface(node ast.Node) bool { +func (r *rpcPlanVisitor) isInterface(node ast.Node) (bool, int) { switch node.Kind { case ast.NodeKindInterfaceTypeDefinition: - return true + return true, node.Ref case ast.NodeKindField: if r.walker.EnclosingTypeDefinition.Kind == ast.NodeKindInterfaceTypeDefinition { - return true + return true, r.walker.EnclosingTypeDefinition.Ref } } - return false + return false, -1 } // LeaveSelectionSet implements astvisitor.SelectionSetVisitor. // It updates the current response field index and response message ancestors. // If the ancestor is an operation definition, it adds the current call to the group. func (r *rpcPlanVisitor) LeaveSelectionSet(ref int) { + if r.walker.Ancestor().Kind == ast.NodeKindInlineFragment { + return + } + if len(r.planInfo.responseFieldIndexAncestors) > 0 { r.planInfo.currentResponseFieldIndex = r.planInfo.responseFieldIndexAncestors[len(r.planInfo.responseFieldIndexAncestors)-1] r.planInfo.responseFieldIndexAncestors = r.planInfo.responseFieldIndexAncestors[:len(r.planInfo.responseFieldIndexAncestors)-1] diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index 7ce0c75974..c12c061331 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -140,26 +140,36 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa root := arena.NewObject() // TODO implement oneof - // if message.OneOf { - // name := strings.ToLower(message.Name) - // oneof := data.Descriptor().Oneofs().ByName(protoref.Name(name)) - // if oneof == nil { - // return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) - // } - - // oneofDescriptor := data.WhichOneof(oneof) - // if oneofDescriptor == nil { - // return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) - // } - - // if oneofDescriptor.Kind() == protoref.MessageKind { - // data = data.Get(oneofDescriptor).Message() - // } - // } + if message.OneOf { + oneof := data.Descriptor().Oneofs().ByName(protoref.Name("instance")) + if oneof == nil { + return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) + } + + oneofDescriptor := data.WhichOneof(oneof) + if oneofDescriptor == nil { + return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) + } + + if oneofDescriptor.Kind() == protoref.MessageKind { + data = data.Get(oneofDescriptor).Message() + } + } for _, field := range message.Fields { if field.StaticValue != "" { - root.Set(field.JSONPath, arena.NewString(field.StaticValue)) + if len(message.ImplementedBy) == 0 { + root.Set(field.JSONPath, arena.NewString(field.StaticValue)) + continue + } + + for _, implementedBy := range message.ImplementedBy { + if implementedBy == string(data.Type().Descriptor().Name()) { + root.Set(field.JSONPath, arena.NewString(implementedBy)) + break + } + } + continue } diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index 718002c383..1b9e2544b0 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -493,180 +493,174 @@ func TestMarshalResponseJSON(t *testing.T) { require.Equal(t, `{"_entities":[{"__typename":"Product","id":"123","name_different":"test","price_different":123.45}]}`, responseJSON.String()) } -// TODO test interface types // Test_DataSource_Load_WithAnimalInterface tests the datasource with Animal interface types (Cat/Dog) // using a bufconn connection to the mock service -// func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { -// // Set up the bufconn listener -// lis := bufconn.Listen(1024 * 1024) - -// // Create a new gRPC server -// server := grpc.NewServer() - -// // Register our mock service implementation -// mockService := &grpctest.MockService{} -// productv1.RegisterProductServiceServer(server, mockService) - -// // Start the server in a goroutine -// go func() { -// if err := server.Serve(lis); err != nil { -// t.Errorf("failed to serve: %v", err) -// } -// }() - -// // Clean up the server when the test completes -// defer server.Stop() - -// // Create a buffer-based dialer -// bufDialer := func(context.Context, string) (net.Conn, error) { -// return lis.Dial() -// } - -// // Connect using bufconn dialer -// conn, err := grpc.Dial( -// "bufnet", -// grpc.WithTransportCredentials(insecure.NewCredentials()), -// grpc.WithContextDialer(bufDialer), -// ) -// require.NoError(t, err) -// defer conn.Close() - -// // Define the GraphQL query for the Animal interface -// query := `query RandomPetQuery { -// randomPet { -// id -// name -// kind -// ... on Cat { -// meowVolume -// } -// ... on Dog { -// barkVolume -// } -// } -// }` - -// report := &operationreport.Report{} - -// // Parse the GraphQL schema -// schemaDoc := ast.NewDocument() -// schemaDoc.Input.ResetInputString(string(grpctest.MustGraphQLSchema(t).RawSchema())) -// astparser.NewParser().Parse(schemaDoc, report) -// require.False(t, report.HasErrors(), "failed to parse schema: %s", report.Error()) - -// // Parse the GraphQL query -// queryDoc := ast.NewDocument() -// queryDoc.Input.ResetInputString(query) -// astparser.NewParser().Parse(queryDoc, report) -// require.False(t, report.HasErrors(), "failed to parse query: %s", report.Error()) - -// // Transform the GraphQL ASTs -// err = asttransform.MergeDefinitionWithBaseSchema(schemaDoc) -// require.NoError(t, err, "failed to merge schema with base") - -// // Create mapping configuration based on the mapping.go -// mapping := &GRPCMapping{ -// Service: "ProductService", -// QueryRPCs: map[string]RPCConfig{ -// "randomPet": { -// RPC: "QueryRandomPet", -// Request: "QueryRandomPetRequest", -// Response: "QueryRandomPetResponse", -// }, -// }, -// Fields: map[string]FieldMap{ -// "Query": { -// "randomPet": { -// TargetName: "random_pet", -// }, -// }, -// "Cat": { -// "id": { -// TargetName: "id", -// }, -// "name": { -// TargetName: "name", -// }, -// "kind": { -// TargetName: "kind", -// }, -// "meowVolume": { -// TargetName: "meow_volume", -// }, -// }, -// "Dog": { -// "id": { -// TargetName: "id", -// }, -// "name": { -// TargetName: "name", -// }, -// "kind": { -// TargetName: "kind", -// }, -// "barkVolume": { -// TargetName: "bark_volume", -// }, -// }, -// }, -// } - -// // Create the datasource -// ds, err := NewDataSource(conn, DataSourceConfig{ -// Operation: queryDoc, -// Definition: schemaDoc, -// ProtoSchema: grpctest.MustProtoSchema(t), -// SubgraphName: "Products", -// Mapping: mapping, -// }) -// require.NoError(t, err) - -// // Execute the query through our datasource -// output := new(bytes.Buffer) -// err = ds.Load(context.Background(), []byte(`{"query":`+fmt.Sprintf("%q", query)+`}`), output) -// require.NoError(t, err) - -// // Print the response for debugging -// responseData := output.String() -// t.Logf("Response: %s", responseData) - -// // Define a response structure that can handle both Cat and Dog types -// type response struct { -// Data struct { -// RandomPet map[string]interface{} `json:"randomPet"` -// } `json:"data"` -// Errors []struct { -// Message string `json:"message"` -// } `json:"errors,omitempty"` -// } - -// var resp response -// err = json.Unmarshal(output.Bytes(), &resp) -// require.NoError(t, err, "Failed to unmarshal response") - -// // Verify there are no errors -// require.Empty(t, resp.Errors, "Response should not contain errors") - -// // Verify we have data -// require.NotNil(t, resp.Data.RandomPet, "RandomPet should not be nil") - -// // Check if we got either a cat or dog by checking for their specific fields -// if _, hasCat := resp.Data.RandomPet["meowVolume"]; hasCat { -// // We got a Cat response -// require.Contains(t, resp.Data.RandomPet, "id") -// require.Contains(t, resp.Data.RandomPet, "name") -// require.Contains(t, resp.Data.RandomPet, "kind") -// require.Contains(t, resp.Data.RandomPet, "meowVolume") -// } else if _, hasDog := resp.Data.RandomPet["barkVolume"]; hasDog { -// // We got a Dog response -// require.Contains(t, resp.Data.RandomPet, "id") -// require.Contains(t, resp.Data.RandomPet, "name") -// require.Contains(t, resp.Data.RandomPet, "kind") -// require.Contains(t, resp.Data.RandomPet, "barkVolume") -// } else { -// t.Fatalf("Response doesn't contain either a Cat or Dog type: %v", resp.Data.RandomPet) -// } -// } +func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { + // 1. Start a gRPC server with our mock implementation + lis, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + // Get the server address + serverAddr := fmt.Sprintf("localhost:%d", lis.Addr().(*net.TCPAddr).Port) + + // Create and start the gRPC server + server := grpc.NewServer() + mockService := &grpctest.MockService{} + productv1.RegisterProductServiceServer(server, mockService) + + go func() { + if err := server.Serve(lis); err != nil { + t.Errorf("failed to serve: %v", err) + } + }() + defer server.Stop() + + // 2. Connect to the gRPC server + // see https://github.com/grpc/grpc-go/issues/7091 + // nolint: staticcheck + conn, err := grpc.Dial( + serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithLocalDNSResolution(), + ) + require.NoError(t, err) + defer conn.Close() + + // Define the GraphQL query for the Animal interface + query := `query RandomPetQuery { + randomPet { + __typename + id + name + kind + ... on Cat { + meowVolume + } + ... on Dog { + barkVolume + } + } + }` + + schemaDoc := grpctest.MustGraphQLSchema(t) + + queryDoc, report := astparser.ParseGraphqlDocumentString(query) + if report.HasErrors() { + t.Fatalf("failed to parse query: %s", report.Error()) + } + + // Create mapping configuration based on the mapping.go + mapping := &GRPCMapping{ + Service: "ProductService", + QueryRPCs: map[string]RPCConfig{ + "randomPet": { + RPC: "QueryRandomPet", + Request: "QueryRandomPetRequest", + Response: "QueryRandomPetResponse", + }, + }, + Fields: map[string]FieldMap{ + "Query": { + "randomPet": { + TargetName: "random_pet", + }, + }, + "Cat": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "meowVolume": { + TargetName: "meow_volume", + }, + }, + "Dog": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "barkVolume": { + TargetName: "bark_volume", + }, + }, + }, + } + + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + if err != nil { + t.Fatalf("failed to compile proto: %v", err) + } + + // Create the datasource + ds, err := NewDataSource(conn, DataSourceConfig{ + Operation: &queryDoc, + Definition: &schemaDoc, + SubgraphName: "Products", + Compiler: compiler, + Mapping: mapping, + }) + require.NoError(t, err) + + // Execute the query through our datasource + output := new(bytes.Buffer) + err = ds.Load(context.Background(), []byte(`{"query":`+fmt.Sprintf("%q", query)+`}`), output) + require.NoError(t, err) + + // Print the response for debugging + responseData := output.String() + t.Logf("Response: %s", responseData) + + // Define a response structure that can handle both Cat and Dog types + type response struct { + Data struct { + RandomPet map[string]interface{} `json:"randomPet"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors,omitempty"` + } + + var resp response + err = json.Unmarshal(output.Bytes(), &resp) + require.NoError(t, err, "Failed to unmarshal response") + + // Verify there are no errors + require.Empty(t, resp.Errors, "Response should not contain errors") + + // Verify we have data + require.NotNil(t, resp.Data.RandomPet, "RandomPet should not be nil") + + // Check if we got either a cat or dog by checking for their specific fields + if _, hasCat := resp.Data.RandomPet["meowVolume"]; hasCat { + // We got a Cat response + require.Contains(t, resp.Data.RandomPet, "__typename") + require.Equal(t, "Cat", resp.Data.RandomPet["__typename"]) + require.Contains(t, resp.Data.RandomPet, "id") + require.Contains(t, resp.Data.RandomPet, "name") + require.Contains(t, resp.Data.RandomPet, "kind") + require.Contains(t, resp.Data.RandomPet, "meowVolume") + } else if _, hasDog := resp.Data.RandomPet["barkVolume"]; hasDog { + // We got a Dog response + require.Contains(t, resp.Data.RandomPet, "__typename") + require.Equal(t, "Dog", resp.Data.RandomPet["__typename"]) + require.Contains(t, resp.Data.RandomPet, "id") + require.Contains(t, resp.Data.RandomPet, "name") + require.Contains(t, resp.Data.RandomPet, "kind") + require.Contains(t, resp.Data.RandomPet, "barkVolume") + } else { + t.Fatalf("Response doesn't contain either a Cat or Dog type: %v", resp.Data.RandomPet) + } +} // Test_DataSource_Load_WithProductQueries tests the product-related query operations func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { diff --git a/v2/pkg/grpctest/product.proto b/v2/pkg/grpctest/product.proto index e0e58eba60..e8ff9a0b85 100644 --- a/v2/pkg/grpctest/product.proto +++ b/v2/pkg/grpctest/product.proto @@ -28,152 +28,184 @@ service ProductService { // Key message for Product entity lookup message LookupProductByIdRequestKey { - // Key field for Product entity lookup + // Key field for Product entity lookup. string id = 1; } -// Request message for Product entity lookup +// Request message for Product entity lookup. message LookupProductByIdRequest { - // List of keys to look up Product entities + /* + * List of keys to look up Product entities. + * Order matters - each key maps to one entity in LookupProductByIdResponse. + */ repeated LookupProductByIdRequestKey keys = 1; } -// Response message for Product entity lookup +// Response message for Product entity lookup. message LookupProductByIdResponse { - // List of Product entities matching the requested keys + /* + * List of Product entities in the same order as the keys in LookupProductByIdRequest. + * Always return the same number of entities as keys. Use null for entities that cannot be found. + * + * Example: + * LookupUserByIdRequest: + * keys: + * - id: 1 + * - id: 2 + * LookupUserByIdResponse: + * result: + * - id: 1 # User with id 1 found + * - null # User with id 2 not found + */ repeated Product result = 1; } // Key message for Storage entity lookup message LookupStorageByIdRequestKey { - // Key field for Storage entity lookup + // Key field for Storage entity lookup. string id = 1; } -// Request message for Storage entity lookup +// Request message for Storage entity lookup. message LookupStorageByIdRequest { - // List of keys to look up Storage entities + /* + * List of keys to look up Storage entities. + * Order matters - each key maps to one entity in LookupStorageByIdResponse. + */ repeated LookupStorageByIdRequestKey keys = 1; } -// Response message for Storage entity lookup +// Response message for Storage entity lookup. message LookupStorageByIdResponse { - // List of Storage entities matching the requested keys + /* + * List of Storage entities in the same order as the keys in LookupStorageByIdRequest. + * Always return the same number of entities as keys. Use null for entities that cannot be found. + * + * Example: + * LookupUserByIdRequest: + * keys: + * - id: 1 + * - id: 2 + * LookupUserByIdResponse: + * result: + * - id: 1 # User with id 1 found + * - null # User with id 2 not found + */ repeated Storage result = 1; } -// Request message for users operation +// Request message for users operation. message QueryUsersRequest { } -// Response message for users operation +// Response message for users operation. message QueryUsersResponse { repeated User users = 1; } -// Request message for user operation +// Request message for user operation. message QueryUserRequest { string id = 1; } -// Response message for user operation +// Response message for user operation. message QueryUserResponse { User user = 1; } -// Request message for nestedType operation +// Request message for nestedType operation. message QueryNestedTypeRequest { } -// Response message for nestedType operation +// Response message for nestedType operation. message QueryNestedTypeResponse { repeated NestedTypeA nested_type = 1; } -// Request message for recursiveType operation +// Request message for recursiveType operation. message QueryRecursiveTypeRequest { } -// Response message for recursiveType operation +// Response message for recursiveType operation. message QueryRecursiveTypeResponse { RecursiveType recursive_type = 1; } -// Request message for typeFilterWithArguments operation +// Request message for typeFilterWithArguments operation. message QueryTypeFilterWithArgumentsRequest { string filter_field_1 = 1; string filter_field_2 = 2; } -// Response message for typeFilterWithArguments operation +// Response message for typeFilterWithArguments operation. message QueryTypeFilterWithArgumentsResponse { repeated TypeWithMultipleFilterFields type_filter_with_arguments = 1; } -// Request message for typeWithMultipleFilterFields operation +// Request message for typeWithMultipleFilterFields operation. message QueryTypeWithMultipleFilterFieldsRequest { FilterTypeInput filter = 1; } -// Response message for typeWithMultipleFilterFields operation +// Response message for typeWithMultipleFilterFields operation. message QueryTypeWithMultipleFilterFieldsResponse { repeated TypeWithMultipleFilterFields type_with_multiple_filter_fields = 1; } -// Request message for complexFilterType operation +// Request message for complexFilterType operation. message QueryComplexFilterTypeRequest { ComplexFilterTypeInput filter = 1; } -// Response message for complexFilterType operation +// Response message for complexFilterType operation. message QueryComplexFilterTypeResponse { repeated TypeWithComplexFilterInput complex_filter_type = 1; } -// Request message for calculateTotals operation +// Request message for calculateTotals operation. message QueryCalculateTotalsRequest { repeated OrderInput orders = 1; } -// Response message for calculateTotals operation +// Response message for calculateTotals operation. message QueryCalculateTotalsResponse { repeated Order calculate_totals = 1; } -// Request message for categories operation +// Request message for categories operation. message QueryCategoriesRequest { } -// Response message for categories operation +// Response message for categories operation. message QueryCategoriesResponse { repeated Category categories = 1; } -// Request message for categoriesByKind operation +// Request message for categoriesByKind operation. message QueryCategoriesByKindRequest { CategoryKind kind = 1; } -// Response message for categoriesByKind operation +// Response message for categoriesByKind operation. message QueryCategoriesByKindResponse { repeated Category categories_by_kind = 1; } -// Request message for categoriesByKinds operation +// Request message for categoriesByKinds operation. message QueryCategoriesByKindsRequest { repeated CategoryKind kinds = 1; } -// Response message for categoriesByKinds operation +// Response message for categoriesByKinds operation. message QueryCategoriesByKindsResponse { repeated Category categories_by_kinds = 1; } -// Request message for filterCategories operation +// Request message for filterCategories operation. message QueryFilterCategoriesRequest { CategoryFilter filter = 1; } -// Response message for filterCategories operation +// Response message for filterCategories operation. message QueryFilterCategoriesResponse { repeated Category filter_categories = 1; } -// Request message for randomPet operation +// Request message for randomPet operation. message QueryRandomPetRequest { } -// Response message for randomPet operation +// Response message for randomPet operation. message QueryRandomPetResponse { Animal random_pet = 1; } -// Request message for allPets operation +// Request message for allPets operation. message QueryAllPetsRequest { } -// Response message for allPets operation +// Response message for allPets operation. message QueryAllPetsResponse { repeated Animal all_pets = 1; } -// Request message for createUser operation +// Request message for createUser operation. message MutationCreateUserRequest { UserInput input = 1; } -// Response message for createUser operation +// Response message for createUser operation. message MutationCreateUserResponse { User create_user = 1; } diff --git a/v2/pkg/grpctest/productv1/product.pb.go b/v2/pkg/grpctest/productv1/product.pb.go index 9d97467e4a..dcfd80e734 100644 --- a/v2/pkg/grpctest/productv1/product.pb.go +++ b/v2/pkg/grpctest/productv1/product.pb.go @@ -79,7 +79,7 @@ func (CategoryKind) EnumDescriptor() ([]byte, []int) { // Key message for Product entity lookup type LookupProductByIdRequestKey struct { state protoimpl.MessageState `protogen:"open.v1"` - // Key field for Product entity lookup + // Key field for Product entity lookup. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -122,10 +122,11 @@ func (x *LookupProductByIdRequestKey) GetId() string { return "" } -// Request message for Product entity lookup +// Request message for Product entity lookup. type LookupProductByIdRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // List of keys to look up Product entities + // List of keys to look up Product entities. + // Order matters - each key maps to one entity in LookupProductByIdResponse. Keys []*LookupProductByIdRequestKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -168,10 +169,22 @@ func (x *LookupProductByIdRequest) GetKeys() []*LookupProductByIdRequestKey { return nil } -// Response message for Product entity lookup +// Response message for Product entity lookup. type LookupProductByIdResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // List of Product entities matching the requested keys + // List of Product entities in the same order as the keys in LookupProductByIdRequest. + // Always return the same number of entities as keys. Use null for entities that cannot be found. + // + // Example: + // + // LookupUserByIdRequest: + // keys: + // - id: 1 + // - id: 2 + // LookupUserByIdResponse: + // result: + // - id: 1 # User with id 1 found + // - null # User with id 2 not found Result []*Product `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -217,7 +230,7 @@ func (x *LookupProductByIdResponse) GetResult() []*Product { // Key message for Storage entity lookup type LookupStorageByIdRequestKey struct { state protoimpl.MessageState `protogen:"open.v1"` - // Key field for Storage entity lookup + // Key field for Storage entity lookup. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -260,10 +273,11 @@ func (x *LookupStorageByIdRequestKey) GetId() string { return "" } -// Request message for Storage entity lookup +// Request message for Storage entity lookup. type LookupStorageByIdRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // List of keys to look up Storage entities + // List of keys to look up Storage entities. + // Order matters - each key maps to one entity in LookupStorageByIdResponse. Keys []*LookupStorageByIdRequestKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -306,10 +320,22 @@ func (x *LookupStorageByIdRequest) GetKeys() []*LookupStorageByIdRequestKey { return nil } -// Response message for Storage entity lookup +// Response message for Storage entity lookup. type LookupStorageByIdResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // List of Storage entities matching the requested keys + // List of Storage entities in the same order as the keys in LookupStorageByIdRequest. + // Always return the same number of entities as keys. Use null for entities that cannot be found. + // + // Example: + // + // LookupUserByIdRequest: + // keys: + // - id: 1 + // - id: 2 + // LookupUserByIdResponse: + // result: + // - id: 1 # User with id 1 found + // - null # User with id 2 not found Result []*Storage `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -352,7 +378,7 @@ func (x *LookupStorageByIdResponse) GetResult() []*Storage { return nil } -// Request message for users operation +// Request message for users operation. type QueryUsersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -389,7 +415,7 @@ func (*QueryUsersRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{6} } -// Response message for users operation +// Response message for users operation. type QueryUsersResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` @@ -434,7 +460,7 @@ func (x *QueryUsersResponse) GetUsers() []*User { return nil } -// Request message for user operation +// Request message for user operation. type QueryUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -479,7 +505,7 @@ func (x *QueryUserRequest) GetId() string { return "" } -// Response message for user operation +// Response message for user operation. type QueryUserResponse struct { state protoimpl.MessageState `protogen:"open.v1"` User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` @@ -524,7 +550,7 @@ func (x *QueryUserResponse) GetUser() *User { return nil } -// Request message for nestedType operation +// Request message for nestedType operation. type QueryNestedTypeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -561,7 +587,7 @@ func (*QueryNestedTypeRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{10} } -// Response message for nestedType operation +// Response message for nestedType operation. type QueryNestedTypeResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NestedType []*NestedTypeA `protobuf:"bytes,1,rep,name=nested_type,json=nestedType,proto3" json:"nested_type,omitempty"` @@ -606,7 +632,7 @@ func (x *QueryNestedTypeResponse) GetNestedType() []*NestedTypeA { return nil } -// Request message for recursiveType operation +// Request message for recursiveType operation. type QueryRecursiveTypeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -643,7 +669,7 @@ func (*QueryRecursiveTypeRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{12} } -// Response message for recursiveType operation +// Response message for recursiveType operation. type QueryRecursiveTypeResponse struct { state protoimpl.MessageState `protogen:"open.v1"` RecursiveType *RecursiveType `protobuf:"bytes,1,opt,name=recursive_type,json=recursiveType,proto3" json:"recursive_type,omitempty"` @@ -688,7 +714,7 @@ func (x *QueryRecursiveTypeResponse) GetRecursiveType() *RecursiveType { return nil } -// Request message for typeFilterWithArguments operation +// Request message for typeFilterWithArguments operation. type QueryTypeFilterWithArgumentsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` FilterField_1 string `protobuf:"bytes,1,opt,name=filter_field_1,json=filterField1,proto3" json:"filter_field_1,omitempty"` @@ -741,7 +767,7 @@ func (x *QueryTypeFilterWithArgumentsRequest) GetFilterField_2() string { return "" } -// Response message for typeFilterWithArguments operation +// Response message for typeFilterWithArguments operation. type QueryTypeFilterWithArgumentsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` TypeFilterWithArguments []*TypeWithMultipleFilterFields `protobuf:"bytes,1,rep,name=type_filter_with_arguments,json=typeFilterWithArguments,proto3" json:"type_filter_with_arguments,omitempty"` @@ -786,7 +812,7 @@ func (x *QueryTypeFilterWithArgumentsResponse) GetTypeFilterWithArguments() []*T return nil } -// Request message for typeWithMultipleFilterFields operation +// Request message for typeWithMultipleFilterFields operation. type QueryTypeWithMultipleFilterFieldsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Filter *FilterTypeInput `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` @@ -831,7 +857,7 @@ func (x *QueryTypeWithMultipleFilterFieldsRequest) GetFilter() *FilterTypeInput return nil } -// Response message for typeWithMultipleFilterFields operation +// Response message for typeWithMultipleFilterFields operation. type QueryTypeWithMultipleFilterFieldsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` TypeWithMultipleFilterFields []*TypeWithMultipleFilterFields `protobuf:"bytes,1,rep,name=type_with_multiple_filter_fields,json=typeWithMultipleFilterFields,proto3" json:"type_with_multiple_filter_fields,omitempty"` @@ -876,7 +902,7 @@ func (x *QueryTypeWithMultipleFilterFieldsResponse) GetTypeWithMultipleFilterFie return nil } -// Request message for complexFilterType operation +// Request message for complexFilterType operation. type QueryComplexFilterTypeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Filter *ComplexFilterTypeInput `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` @@ -921,7 +947,7 @@ func (x *QueryComplexFilterTypeRequest) GetFilter() *ComplexFilterTypeInput { return nil } -// Response message for complexFilterType operation +// Response message for complexFilterType operation. type QueryComplexFilterTypeResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ComplexFilterType []*TypeWithComplexFilterInput `protobuf:"bytes,1,rep,name=complex_filter_type,json=complexFilterType,proto3" json:"complex_filter_type,omitempty"` @@ -966,7 +992,7 @@ func (x *QueryComplexFilterTypeResponse) GetComplexFilterType() []*TypeWithCompl return nil } -// Request message for calculateTotals operation +// Request message for calculateTotals operation. type QueryCalculateTotalsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Orders []*OrderInput `protobuf:"bytes,1,rep,name=orders,proto3" json:"orders,omitempty"` @@ -1011,7 +1037,7 @@ func (x *QueryCalculateTotalsRequest) GetOrders() []*OrderInput { return nil } -// Response message for calculateTotals operation +// Response message for calculateTotals operation. type QueryCalculateTotalsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` CalculateTotals []*Order `protobuf:"bytes,1,rep,name=calculate_totals,json=calculateTotals,proto3" json:"calculate_totals,omitempty"` @@ -1056,7 +1082,7 @@ func (x *QueryCalculateTotalsResponse) GetCalculateTotals() []*Order { return nil } -// Request message for categories operation +// Request message for categories operation. type QueryCategoriesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1093,7 +1119,7 @@ func (*QueryCategoriesRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{22} } -// Response message for categories operation +// Response message for categories operation. type QueryCategoriesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Categories []*Category `protobuf:"bytes,1,rep,name=categories,proto3" json:"categories,omitempty"` @@ -1138,7 +1164,7 @@ func (x *QueryCategoriesResponse) GetCategories() []*Category { return nil } -// Request message for categoriesByKind operation +// Request message for categoriesByKind operation. type QueryCategoriesByKindRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Kind CategoryKind `protobuf:"varint,1,opt,name=kind,proto3,enum=productv1.CategoryKind" json:"kind,omitempty"` @@ -1183,7 +1209,7 @@ func (x *QueryCategoriesByKindRequest) GetKind() CategoryKind { return CategoryKind_CATEGORY_KIND_UNSPECIFIED } -// Response message for categoriesByKind operation +// Response message for categoriesByKind operation. type QueryCategoriesByKindResponse struct { state protoimpl.MessageState `protogen:"open.v1"` CategoriesByKind []*Category `protobuf:"bytes,1,rep,name=categories_by_kind,json=categoriesByKind,proto3" json:"categories_by_kind,omitempty"` @@ -1228,7 +1254,7 @@ func (x *QueryCategoriesByKindResponse) GetCategoriesByKind() []*Category { return nil } -// Request message for categoriesByKinds operation +// Request message for categoriesByKinds operation. type QueryCategoriesByKindsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Kinds []CategoryKind `protobuf:"varint,1,rep,packed,name=kinds,proto3,enum=productv1.CategoryKind" json:"kinds,omitempty"` @@ -1273,7 +1299,7 @@ func (x *QueryCategoriesByKindsRequest) GetKinds() []CategoryKind { return nil } -// Response message for categoriesByKinds operation +// Response message for categoriesByKinds operation. type QueryCategoriesByKindsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` CategoriesByKinds []*Category `protobuf:"bytes,1,rep,name=categories_by_kinds,json=categoriesByKinds,proto3" json:"categories_by_kinds,omitempty"` @@ -1318,7 +1344,7 @@ func (x *QueryCategoriesByKindsResponse) GetCategoriesByKinds() []*Category { return nil } -// Request message for filterCategories operation +// Request message for filterCategories operation. type QueryFilterCategoriesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Filter *CategoryFilter `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` @@ -1363,7 +1389,7 @@ func (x *QueryFilterCategoriesRequest) GetFilter() *CategoryFilter { return nil } -// Response message for filterCategories operation +// Response message for filterCategories operation. type QueryFilterCategoriesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` FilterCategories []*Category `protobuf:"bytes,1,rep,name=filter_categories,json=filterCategories,proto3" json:"filter_categories,omitempty"` @@ -1408,7 +1434,7 @@ func (x *QueryFilterCategoriesResponse) GetFilterCategories() []*Category { return nil } -// Request message for randomPet operation +// Request message for randomPet operation. type QueryRandomPetRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1445,7 +1471,7 @@ func (*QueryRandomPetRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{30} } -// Response message for randomPet operation +// Response message for randomPet operation. type QueryRandomPetResponse struct { state protoimpl.MessageState `protogen:"open.v1"` RandomPet *Animal `protobuf:"bytes,1,opt,name=random_pet,json=randomPet,proto3" json:"random_pet,omitempty"` @@ -1490,7 +1516,7 @@ func (x *QueryRandomPetResponse) GetRandomPet() *Animal { return nil } -// Request message for allPets operation +// Request message for allPets operation. type QueryAllPetsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1527,7 +1553,7 @@ func (*QueryAllPetsRequest) Descriptor() ([]byte, []int) { return file_product_proto_rawDescGZIP(), []int{32} } -// Response message for allPets operation +// Response message for allPets operation. type QueryAllPetsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AllPets []*Animal `protobuf:"bytes,1,rep,name=all_pets,json=allPets,proto3" json:"all_pets,omitempty"` @@ -1572,7 +1598,7 @@ func (x *QueryAllPetsResponse) GetAllPets() []*Animal { return nil } -// Request message for createUser operation +// Request message for createUser operation. type MutationCreateUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Input *UserInput `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` @@ -1617,7 +1643,7 @@ func (x *MutationCreateUserRequest) GetInput() *UserInput { return nil } -// Response message for createUser operation +// Response message for createUser operation. type MutationCreateUserResponse struct { state protoimpl.MessageState `protogen:"open.v1"` CreateUser *User `protobuf:"bytes,1,opt,name=create_user,json=createUser,proto3" json:"create_user,omitempty"` From 45b02dc7dc1f870bd09a07ce26c21486f83538e7 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 30 Jun 2025 16:25:37 +0200 Subject: [PATCH 02/13] feat: add support for union types --- .../grpc_datasource/execution_plan.go | 41 +- .../grpc_datasource/execution_plan_test.go | 6 +- .../grpc_datasource/execution_plan_visitor.go | 46 +- .../grpc_datasource/grpc_datasource.go | 9 +- .../grpc_datasource/grpc_datasource_test.go | 200 +++ v2/pkg/grpctest/mapping/mapping.go | 62 + v2/pkg/grpctest/mockservice.go | 154 ++ v2/pkg/grpctest/product.proto | 61 + v2/pkg/grpctest/productv1/product.pb.go | 1326 +++++++++++++---- v2/pkg/grpctest/productv1/product_grpc.pb.go | 114 ++ v2/pkg/grpctest/testdata/products.graphqls | 36 + 11 files changed, 1732 insertions(+), 323 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index c15d361fc6..0faebc2658 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -13,6 +13,34 @@ const ( federationKeyDirectiveName = "key" ) +// OneOfType represents the type of a oneof field in a protobuf message. +// It can be either an interface or a union type. +type OneOfType int + +// FieldName returns the corresponding field name for the OneOfType. +// For interfaces, it returns "instance", for unions it returns "value". +// Returns an empty string for invalid or unknown types. +func (o OneOfType) FieldName() string { + switch o { + case OneOfTypeInterface: + return "instance" + case OneOfTypeUnion: + return "value" + } + + return "" +} + +// OneOfType constants define the different types of oneof fields. +const ( + // OneOfTypeNone represents no oneof type (default/zero value) + OneOfTypeNone OneOfType = iota + // OneOfTypeInterface represents an interface type oneof field + OneOfTypeInterface + // OneOfTypeUnion represents a union type oneof field + OneOfTypeUnion +) + // RPCExecutionPlan represents a plan for executing one or more RPC calls // to gRPC services. It defines the sequence of calls and their dependencies. type RPCExecutionPlan struct { @@ -46,10 +74,15 @@ type RPCMessage struct { Name string // Fields is a list of fields in the message Fields RPCFields - // OneOf indicates if the message is an interface - OneOf bool - // ImplementedBy provides the names of the types that are implemented by the Interface or Union - ImplementedBy []string + // OneOfType indicates the type of the oneof field + OneOfType OneOfType + // MemberTypes provides the names of the types that are implemented by the Interface or Union + MemberTypes []string +} + +// IsOneOf checks if the message is a oneof field. +func (r *RPCMessage) IsOneOf() bool { + return r.OneOfType > OneOfTypeNone && r.OneOfType <= OneOfTypeUnion } // RPCField represents a single field in a gRPC message. diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index 9f6e2d463a..bb7dc353c1 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -1514,9 +1514,9 @@ func TestInterfaceExecutionPlan(t *testing.T) { TypeName: string(DataTypeMessage), JSONPath: "randomPet", Message: &RPCMessage{ - Name: "Animal", - OneOf: true, - ImplementedBy: []string{ + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ "Cat", "Dog", }, diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go index a5528b267b..de7ad9e6a7 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go @@ -158,9 +158,13 @@ func (r *rpcPlanVisitor) EnterSelectionSet(ref int) { r.planInfo.responseMessageAncestors = append(r.planInfo.responseMessageAncestors, r.planInfo.currentResponseMessage) r.planInfo.currentResponseMessage = r.planInfo.currentResponseMessage.Fields[r.planInfo.currentResponseFieldIndex].Message - if isInterface, interfaceRef := r.isInterface(r.walker.Ancestor()); isInterface { - r.planInfo.currentResponseMessage.OneOf = true - r.planInfo.currentResponseMessage.ImplementedBy, _ = r.definition.InterfaceTypeDefinitionImplementedByObjectWithNames(interfaceRef) + // Check if the ancestor type is a composite type (interface or union) + // and set the oneof type and member types. + if err := r.handleCompositeType(r.walker.Ancestor()); err != nil { + // If the ancestor is a composite type, but we were unable to resolve the member types, + // we stop the walker and return an internal error. + r.walker.StopWithInternalErr(err) + return } // Keep track of the field indices for the current response message. @@ -171,17 +175,41 @@ func (r *rpcPlanVisitor) EnterSelectionSet(ref int) { r.planInfo.currentResponseFieldIndex = 0 // reset the field index for the current selection set } -func (r *rpcPlanVisitor) isInterface(node ast.Node) (bool, int) { +func (r *rpcPlanVisitor) handleCompositeType(node ast.Node) error { + if node.Ref < 0 { + return nil + } + + var ( + ok bool + oneOfType OneOfType + memberTypes []string + ) + switch node.Kind { - case ast.NodeKindInterfaceTypeDefinition: - return true, node.Ref case ast.NodeKindField: - if r.walker.EnclosingTypeDefinition.Kind == ast.NodeKindInterfaceTypeDefinition { - return true, r.walker.EnclosingTypeDefinition.Ref + r.handleCompositeType(r.walker.EnclosingTypeDefinition) + return nil + case ast.NodeKindInterfaceTypeDefinition: + oneOfType = OneOfTypeInterface + memberTypes, ok = r.definition.InterfaceTypeDefinitionImplementedByObjectWithNames(node.Ref) + if !ok { + return fmt.Errorf("interface type %s is not implemented by any object", r.definition.InterfaceTypeDefinitionNameString(node.Ref)) + } + case ast.NodeKindUnionTypeDefinition: + oneOfType = OneOfTypeUnion + memberTypes, ok = r.definition.UnionTypeDefinitionMemberTypeNames(node.Ref) + if !ok { + return fmt.Errorf("union type %s is not defined", r.definition.UnionTypeDefinitionNameString(node.Ref)) } + default: + return nil } - return false, -1 + r.planInfo.currentResponseMessage.OneOfType = oneOfType + r.planInfo.currentResponseMessage.MemberTypes = memberTypes + + return nil } // LeaveSelectionSet implements astvisitor.SelectionSetVisitor. diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index c12c061331..f7e86bfbbc 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -139,9 +139,8 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa root := arena.NewObject() - // TODO implement oneof - if message.OneOf { - oneof := data.Descriptor().Oneofs().ByName(protoref.Name("instance")) + if message.IsOneOf() { + oneof := data.Descriptor().Oneofs().ByName(protoref.Name(message.OneOfType.FieldName())) if oneof == nil { return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) } @@ -158,12 +157,12 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa for _, field := range message.Fields { if field.StaticValue != "" { - if len(message.ImplementedBy) == 0 { + if len(message.MemberTypes) == 0 { root.Set(field.JSONPath, arena.NewString(field.StaticValue)) continue } - for _, implementedBy := range message.ImplementedBy { + for _, implementedBy := range message.MemberTypes { if implementedBy == string(data.Type().Descriptor().Name()) { root.Set(field.JSONPath, arena.NewString(implementedBy)) break diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index 1b9e2544b0..156976b6b8 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -662,7 +662,207 @@ func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { } } +func Test_Datasource_Load_WithUnionTypes(t *testing.T) { + // Set up the bufconn listener + lis := bufconn.Listen(1024 * 1024) + + // Create a new gRPC server + server := grpc.NewServer() + + // Register our mock service implementation + mockService := &grpctest.MockService{} + productv1.RegisterProductServiceServer(server, mockService) + + // Start the server in a goroutine + go func() { + if err := server.Serve(lis); err != nil { + t.Errorf("failed to serve: %v", err) + } + }() + + // Clean up the server when the test completes + defer server.Stop() + + // Create a buffer-based dialer + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + // Connect using bufconn dialer + // see https://github.com/grpc/grpc-go/issues/7091 + // nolint: staticcheck + conn, err := grpc.Dial( + "bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(bufDialer), + grpc.WithLocalDNSResolution(), + ) + require.NoError(t, err) + defer conn.Close() + + testCases := []struct { + name string + query string + vars string + validate func(t *testing.T, data map[string]interface{}) + }{ + { + name: "Query random search result", + query: `query { randomSearchResult { __typename ... on Product { id name price } ... on User { id name } ... on Category { id name kind } } }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + searchResult, ok := data["randomSearchResult"].(map[string]interface{}) + require.True(t, ok, "randomSearchResult should be an object") + require.NotEmpty(t, searchResult, "randomSearchResult should not be empty") + require.Contains(t, searchResult, "__typename") + typeName := searchResult["__typename"].(string) + + switch typeName { + case "Product": + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + require.Contains(t, searchResult, "price") + case "User": + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + case "Category": + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + require.Contains(t, searchResult, "kind") + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Parse the GraphQL schema + schemaDoc := grpctest.MustGraphQLSchema(t) + + // Parse the GraphQL query + queryDoc, report := astparser.ParseGraphqlDocumentString(tc.query) + if report.HasErrors() { + t.Fatalf("failed to parse query: %s", report.Error()) + } + + mapping := &GRPCMapping{ + Service: "ProductService", + QueryRPCs: map[string]RPCConfig{ + "randomSearchResult": { + RPC: "QueryRandomSearchResult", + Request: "QueryRandomSearchResultRequest", + Response: "QueryRandomSearchResultResponse", + }, + "search": { + RPC: "QuerySearch", + Request: "QuerySearchRequest", + Response: "QuerySearchResponse", + }, + }, + MutationRPCs: map[string]RPCConfig{ + "performAction": { + RPC: "MutationPerformAction", + Request: "MutationPerformActionRequest", + Response: "MutationPerformActionResponse", + }, + }, + Fields: map[string]FieldMap{ + "Query": { + "randomSearchResult": { + TargetName: "random_search_result", + }, + "search": { + TargetName: "search", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + }, + "Mutation": { + "performAction": { + TargetName: "perform_action", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + }, + "ActionSuccess": { + "message": { + TargetName: "message", + }, + "timestamp": { + TargetName: "timestamp", + }, + }, + "ActionError": { + "message": { + TargetName: "message", + }, + "code": { + TargetName: "code", + }, + }, + "SearchInput": { + "query": { + TargetName: "query", + }, + "limit": { + TargetName: "limit", + }, + }, + "ActionInput": { + "type": { + TargetName: "type", + }, + "payload": { + TargetName: "payload", + }, + }, + }, + } + + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + if err != nil { + t.Fatalf("failed to compile proto: %v", err) + } + + // Create the datasource + ds, err := NewDataSource(conn, DataSourceConfig{ + Operation: &queryDoc, + Definition: &schemaDoc, + SubgraphName: "Products", + Mapping: mapping, + Compiler: compiler, + }) + require.NoError(t, err) + + // Execute the query through our datasource + output := new(bytes.Buffer) + input := fmt.Sprintf(`{"query":%q,"body":%s}`, tc.query, tc.vars) + err = ds.Load(context.Background(), []byte(input), output) + require.NoError(t, err) + + // Parse the response + var resp struct { + Data map[string]interface{} `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors,omitempty"` + } + + err = json.Unmarshal(output.Bytes(), &resp) + require.NoError(t, err, "Failed to unmarshal response") + require.Empty(t, resp.Errors, "Response should not contain errors") + require.NotEmpty(t, resp.Data, "Response should contain data") + + // Run the validation function + tc.validate(t, resp.Data) + }) + } +} + // Test_DataSource_Load_WithProductQueries tests the product-related query operations +// Category queries are used to mainly focus on testing Enum values func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { // Set up the bufconn listener lis := bufconn.Listen(1024 * 1024) diff --git a/v2/pkg/grpctest/mapping/mapping.go b/v2/pkg/grpctest/mapping/mapping.go index 7711fcd7d6..9282fa537e 100644 --- a/v2/pkg/grpctest/mapping/mapping.go +++ b/v2/pkg/grpctest/mapping/mapping.go @@ -76,6 +76,16 @@ func DefaultGRPCMapping() *grpcdatasource.GRPCMapping { Request: "QueryFilterCategoriesRequest", Response: "QueryFilterCategoriesResponse", }, + "randomSearchResult": { + RPC: "QueryRandomSearchResult", + Request: "QueryRandomSearchResultRequest", + Response: "QueryRandomSearchResultResponse", + }, + "search": { + RPC: "QuerySearch", + Request: "QuerySearchRequest", + Response: "QuerySearchResponse", + }, }, MutationRPCs: grpcdatasource.RPCConfigMap{ "createUser": { @@ -83,6 +93,11 @@ func DefaultGRPCMapping() *grpcdatasource.GRPCMapping { Request: "CreateUserRequest", Response: "CreateUserResponse", }, + "performAction": { + RPC: "MutationPerformAction", + Request: "MutationPerformActionRequest", + Response: "MutationPerformActionResponse", + }, }, SubscriptionRPCs: grpcdatasource.RPCConfigMap{}, EntityRPCs: map[string]grpcdatasource.EntityRPCConfig{ @@ -171,6 +186,15 @@ func DefaultGRPCMapping() *grpcdatasource.GRPCMapping { "orders": "orders", }, }, + "search": { + TargetName: "search", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + "randomSearchResult": { + TargetName: "random_search_result", + }, }, "Mutation": { "createUser": { @@ -179,6 +203,12 @@ func DefaultGRPCMapping() *grpcdatasource.GRPCMapping { "input": "input", }, }, + "performAction": { + TargetName: "perform_action", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, }, "UserInput": { "name": { @@ -407,6 +437,38 @@ func DefaultGRPCMapping() *grpcdatasource.GRPCMapping { TargetName: "modifiers", }, }, + "ActionSuccess": { + "message": { + TargetName: "message", + }, + "timestamp": { + TargetName: "timestamp", + }, + }, + "ActionError": { + "message": { + TargetName: "message", + }, + "code": { + TargetName: "code", + }, + }, + "SearchInput": { + "query": { + TargetName: "query", + }, + "limit": { + TargetName: "limit", + }, + }, + "ActionInput": { + "type": { + TargetName: "type", + }, + "payload": { + TargetName: "payload", + }, + }, }, } } diff --git a/v2/pkg/grpctest/mockservice.go b/v2/pkg/grpctest/mockservice.go index 43e462ff7d..7664cae011 100644 --- a/v2/pkg/grpctest/mockservice.go +++ b/v2/pkg/grpctest/mockservice.go @@ -11,10 +11,164 @@ import ( "google.golang.org/grpc/status" ) +var _ productv1.ProductServiceServer = &MockService{} + type MockService struct { productv1.UnimplementedProductServiceServer } +// MutationPerformAction implements productv1.ProductServiceServer. +func (s *MockService) MutationPerformAction(ctx context.Context, in *productv1.MutationPerformActionRequest) (*productv1.MutationPerformActionResponse, error) { + input := in.GetInput() + actionType := input.GetType() + + // Simulate different action results based on the action type + var result *productv1.ActionResult + + switch actionType { + case "error_action": + // Return an error result + result = &productv1.ActionResult{ + Value: &productv1.ActionResult_ActionError{ + ActionError: &productv1.ActionError{ + Message: "Action failed due to validation error", + Code: "VALIDATION_ERROR", + }, + }, + } + case "invalid_action": + // Return a different error result + result = &productv1.ActionResult{ + Value: &productv1.ActionResult_ActionError{ + ActionError: &productv1.ActionError{ + Message: "Invalid action type provided", + Code: "INVALID_ACTION", + }, + }, + } + default: + // Return a success result + result = &productv1.ActionResult{ + Value: &productv1.ActionResult_ActionSuccess{ + ActionSuccess: &productv1.ActionSuccess{ + Message: fmt.Sprintf("Action '%s' completed successfully", actionType), + Timestamp: "2024-01-01T00:00:00Z", + }, + }, + } + } + + return &productv1.MutationPerformActionResponse{ + PerformAction: result, + }, nil +} + +// QueryRandomSearchResult implements productv1.ProductServiceServer. +func (s *MockService) QueryRandomSearchResult(ctx context.Context, in *productv1.QueryRandomSearchResultRequest) (*productv1.QueryRandomSearchResultResponse, error) { + // Randomly return one of the three union types + var result *productv1.SearchResult + + switch rand.Intn(3) { + case 0: + // Return a Product + result = &productv1.SearchResult{ + Value: &productv1.SearchResult_Product{ + Product: &productv1.Product{ + Id: "product-random-1", + Name: "Random Product", + Price: 29.99, + }, + }, + } + case 1: + // Return a User + result = &productv1.SearchResult{ + Value: &productv1.SearchResult_User{ + User: &productv1.User{ + Id: "user-random-1", + Name: "Random User", + }, + }, + } + default: + // Return a Category + result = &productv1.SearchResult{ + Value: &productv1.SearchResult_Category{ + Category: &productv1.Category{ + Id: "category-random-1", + Name: "Random Category", + Kind: productv1.CategoryKind_CATEGORY_KIND_ELECTRONICS, + }, + }, + } + } + + return &productv1.QueryRandomSearchResultResponse{ + RandomSearchResult: result, + }, nil +} + +// QuerySearch implements productv1.ProductServiceServer. +func (s *MockService) QuerySearch(ctx context.Context, in *productv1.QuerySearchRequest) (*productv1.QuerySearchResponse, error) { + input := in.GetInput() + query := input.GetQuery() + limit := input.GetLimit() + + // Default limit if not specified + if limit <= 0 { + limit = 10 + } + + var results []*productv1.SearchResult + + // Generate a mix of different union types based on the query + for i := int32(0); i < limit && i < 6; i++ { // Cap at 6 results for testing + switch i % 3 { + case 0: + // Add a Product + results = append(results, &productv1.SearchResult{ + Value: &productv1.SearchResult_Product{ + Product: &productv1.Product{ + Id: fmt.Sprintf("product-search-%d", i+1), + Name: fmt.Sprintf("Product matching '%s' #%d", query, i+1), + Price: float64(10 + i*5), + }, + }, + }) + case 1: + // Add a User + results = append(results, &productv1.SearchResult{ + Value: &productv1.SearchResult_User{ + User: &productv1.User{ + Id: fmt.Sprintf("user-search-%d", i+1), + Name: fmt.Sprintf("User matching '%s' #%d", query, i+1), + }, + }, + }) + case 2: + // Add a Category + kinds := []productv1.CategoryKind{ + productv1.CategoryKind_CATEGORY_KIND_BOOK, + productv1.CategoryKind_CATEGORY_KIND_ELECTRONICS, + productv1.CategoryKind_CATEGORY_KIND_FURNITURE, + } + results = append(results, &productv1.SearchResult{ + Value: &productv1.SearchResult_Category{ + Category: &productv1.Category{ + Id: fmt.Sprintf("category-search-%d", i+1), + Name: fmt.Sprintf("Category matching '%s' #%d", query, i+1), + Kind: kinds[i%int32(len(kinds))], + }, + }, + }) + } + } + + return &productv1.QuerySearchResponse{ + Search: results, + }, nil +} + func (s *MockService) LookupProductById(ctx context.Context, in *productv1.LookupProductByIdRequest) (*productv1.LookupProductByIdResponse, error) { var results []*productv1.Product diff --git a/v2/pkg/grpctest/product.proto b/v2/pkg/grpctest/product.proto index e8ff9a0b85..39ce8fbf55 100644 --- a/v2/pkg/grpctest/product.proto +++ b/v2/pkg/grpctest/product.proto @@ -10,6 +10,7 @@ service ProductService { // Lookup Storage entity by id rpc LookupStorageById(LookupStorageByIdRequest) returns (LookupStorageByIdResponse) {} rpc MutationCreateUser(MutationCreateUserRequest) returns (MutationCreateUserResponse) {} + rpc MutationPerformAction(MutationPerformActionRequest) returns (MutationPerformActionResponse) {} rpc QueryAllPets(QueryAllPetsRequest) returns (QueryAllPetsResponse) {} rpc QueryCalculateTotals(QueryCalculateTotalsRequest) returns (QueryCalculateTotalsResponse) {} rpc QueryCategories(QueryCategoriesRequest) returns (QueryCategoriesResponse) {} @@ -19,7 +20,9 @@ service ProductService { rpc QueryFilterCategories(QueryFilterCategoriesRequest) returns (QueryFilterCategoriesResponse) {} rpc QueryNestedType(QueryNestedTypeRequest) returns (QueryNestedTypeResponse) {} rpc QueryRandomPet(QueryRandomPetRequest) returns (QueryRandomPetResponse) {} + rpc QueryRandomSearchResult(QueryRandomSearchResultRequest) returns (QueryRandomSearchResultResponse) {} rpc QueryRecursiveType(QueryRecursiveTypeRequest) returns (QueryRecursiveTypeResponse) {} + rpc QuerySearch(QuerySearchRequest) returns (QuerySearchResponse) {} rpc QueryTypeFilterWithArguments(QueryTypeFilterWithArgumentsRequest) returns (QueryTypeFilterWithArgumentsResponse) {} rpc QueryTypeWithMultipleFilterFields(QueryTypeWithMultipleFilterFieldsRequest) returns (QueryTypeWithMultipleFilterFieldsResponse) {} rpc QueryUser(QueryUserRequest) returns (QueryUserResponse) {} @@ -201,6 +204,21 @@ message QueryAllPetsRequest { message QueryAllPetsResponse { repeated Animal all_pets = 1; } +// Request message for search operation. +message QuerySearchRequest { + SearchInput input = 1; +} +// Response message for search operation. +message QuerySearchResponse { + repeated SearchResult search = 1; +} +// Request message for randomSearchResult operation. +message QueryRandomSearchResultRequest { +} +// Response message for randomSearchResult operation. +message QueryRandomSearchResultResponse { + SearchResult random_search_result = 1; +} // Request message for createUser operation. message MutationCreateUserRequest { UserInput input = 1; @@ -209,6 +227,14 @@ message MutationCreateUserRequest { message MutationCreateUserResponse { User create_user = 1; } +// Request message for performAction operation. +message MutationPerformActionRequest { + ActionInput input = 1; +} +// Response message for performAction operation. +message MutationPerformActionResponse { + ActionResult perform_action = 1; +} message Product { string id = 1; @@ -291,10 +317,35 @@ message Animal { } } +message SearchInput { + string query = 1; + int32 limit = 2; +} + +message SearchResult { + oneof value { + Product product = 1; + User user = 2; + Category category = 3; + } +} + message UserInput { string name = 1; } +message ActionInput { + string type = 1; + string payload = 2; +} + +message ActionResult { + oneof value { + ActionSuccess action_success = 1; + ActionError action_error = 2; + } +} + message NestedTypeB { string id = 1; string name = 2; @@ -350,4 +401,14 @@ message Dog { string name = 2; string kind = 3; int32 bark_volume = 4; +} + +message ActionSuccess { + string message = 1; + string timestamp = 2; +} + +message ActionError { + string message = 1; + string code = 2; } \ No newline at end of file diff --git a/v2/pkg/grpctest/productv1/product.pb.go b/v2/pkg/grpctest/productv1/product.pb.go index dcfd80e734..7473b17855 100644 --- a/v2/pkg/grpctest/productv1/product.pb.go +++ b/v2/pkg/grpctest/productv1/product.pb.go @@ -1598,6 +1598,178 @@ func (x *QueryAllPetsResponse) GetAllPets() []*Animal { return nil } +// Request message for search operation. +type QuerySearchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Input *SearchInput `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuerySearchRequest) Reset() { + *x = QuerySearchRequest{} + mi := &file_product_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuerySearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuerySearchRequest) ProtoMessage() {} + +func (x *QuerySearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuerySearchRequest.ProtoReflect.Descriptor instead. +func (*QuerySearchRequest) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{34} +} + +func (x *QuerySearchRequest) GetInput() *SearchInput { + if x != nil { + return x.Input + } + return nil +} + +// Response message for search operation. +type QuerySearchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Search []*SearchResult `protobuf:"bytes,1,rep,name=search,proto3" json:"search,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuerySearchResponse) Reset() { + *x = QuerySearchResponse{} + mi := &file_product_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuerySearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuerySearchResponse) ProtoMessage() {} + +func (x *QuerySearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuerySearchResponse.ProtoReflect.Descriptor instead. +func (*QuerySearchResponse) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{35} +} + +func (x *QuerySearchResponse) GetSearch() []*SearchResult { + if x != nil { + return x.Search + } + return nil +} + +// Request message for randomSearchResult operation. +type QueryRandomSearchResultRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRandomSearchResultRequest) Reset() { + *x = QueryRandomSearchResultRequest{} + mi := &file_product_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRandomSearchResultRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRandomSearchResultRequest) ProtoMessage() {} + +func (x *QueryRandomSearchResultRequest) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRandomSearchResultRequest.ProtoReflect.Descriptor instead. +func (*QueryRandomSearchResultRequest) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{36} +} + +// Response message for randomSearchResult operation. +type QueryRandomSearchResultResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RandomSearchResult *SearchResult `protobuf:"bytes,1,opt,name=random_search_result,json=randomSearchResult,proto3" json:"random_search_result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRandomSearchResultResponse) Reset() { + *x = QueryRandomSearchResultResponse{} + mi := &file_product_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRandomSearchResultResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRandomSearchResultResponse) ProtoMessage() {} + +func (x *QueryRandomSearchResultResponse) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRandomSearchResultResponse.ProtoReflect.Descriptor instead. +func (*QueryRandomSearchResultResponse) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{37} +} + +func (x *QueryRandomSearchResultResponse) GetRandomSearchResult() *SearchResult { + if x != nil { + return x.RandomSearchResult + } + return nil +} + // Request message for createUser operation. type MutationCreateUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1608,7 +1780,7 @@ type MutationCreateUserRequest struct { func (x *MutationCreateUserRequest) Reset() { *x = MutationCreateUserRequest{} - mi := &file_product_proto_msgTypes[34] + mi := &file_product_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1620,7 +1792,7 @@ func (x *MutationCreateUserRequest) String() string { func (*MutationCreateUserRequest) ProtoMessage() {} func (x *MutationCreateUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[34] + mi := &file_product_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1633,7 +1805,7 @@ func (x *MutationCreateUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MutationCreateUserRequest.ProtoReflect.Descriptor instead. func (*MutationCreateUserRequest) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{34} + return file_product_proto_rawDescGZIP(), []int{38} } func (x *MutationCreateUserRequest) GetInput() *UserInput { @@ -1653,7 +1825,7 @@ type MutationCreateUserResponse struct { func (x *MutationCreateUserResponse) Reset() { *x = MutationCreateUserResponse{} - mi := &file_product_proto_msgTypes[35] + mi := &file_product_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1665,7 +1837,7 @@ func (x *MutationCreateUserResponse) String() string { func (*MutationCreateUserResponse) ProtoMessage() {} func (x *MutationCreateUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[35] + mi := &file_product_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1678,7 +1850,7 @@ func (x *MutationCreateUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MutationCreateUserResponse.ProtoReflect.Descriptor instead. func (*MutationCreateUserResponse) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{35} + return file_product_proto_rawDescGZIP(), []int{39} } func (x *MutationCreateUserResponse) GetCreateUser() *User { @@ -1688,6 +1860,96 @@ func (x *MutationCreateUserResponse) GetCreateUser() *User { return nil } +// Request message for performAction operation. +type MutationPerformActionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Input *ActionInput `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutationPerformActionRequest) Reset() { + *x = MutationPerformActionRequest{} + mi := &file_product_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutationPerformActionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutationPerformActionRequest) ProtoMessage() {} + +func (x *MutationPerformActionRequest) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutationPerformActionRequest.ProtoReflect.Descriptor instead. +func (*MutationPerformActionRequest) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{40} +} + +func (x *MutationPerformActionRequest) GetInput() *ActionInput { + if x != nil { + return x.Input + } + return nil +} + +// Response message for performAction operation. +type MutationPerformActionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PerformAction *ActionResult `protobuf:"bytes,1,opt,name=perform_action,json=performAction,proto3" json:"perform_action,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutationPerformActionResponse) Reset() { + *x = MutationPerformActionResponse{} + mi := &file_product_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutationPerformActionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutationPerformActionResponse) ProtoMessage() {} + +func (x *MutationPerformActionResponse) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutationPerformActionResponse.ProtoReflect.Descriptor instead. +func (*MutationPerformActionResponse) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{41} +} + +func (x *MutationPerformActionResponse) GetPerformAction() *ActionResult { + if x != nil { + return x.PerformAction + } + return nil +} + type Product struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -1699,7 +1961,7 @@ type Product struct { func (x *Product) Reset() { *x = Product{} - mi := &file_product_proto_msgTypes[36] + mi := &file_product_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1711,7 +1973,7 @@ func (x *Product) String() string { func (*Product) ProtoMessage() {} func (x *Product) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[36] + mi := &file_product_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1724,7 +1986,7 @@ func (x *Product) ProtoReflect() protoreflect.Message { // Deprecated: Use Product.ProtoReflect.Descriptor instead. func (*Product) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{36} + return file_product_proto_rawDescGZIP(), []int{42} } func (x *Product) GetId() string { @@ -1759,7 +2021,7 @@ type Storage struct { func (x *Storage) Reset() { *x = Storage{} - mi := &file_product_proto_msgTypes[37] + mi := &file_product_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1771,7 +2033,7 @@ func (x *Storage) String() string { func (*Storage) ProtoMessage() {} func (x *Storage) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[37] + mi := &file_product_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1784,7 +2046,7 @@ func (x *Storage) ProtoReflect() protoreflect.Message { // Deprecated: Use Storage.ProtoReflect.Descriptor instead. func (*Storage) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{37} + return file_product_proto_rawDescGZIP(), []int{43} } func (x *Storage) GetId() string { @@ -1818,7 +2080,7 @@ type User struct { func (x *User) Reset() { *x = User{} - mi := &file_product_proto_msgTypes[38] + mi := &file_product_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1830,7 +2092,7 @@ func (x *User) String() string { func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[38] + mi := &file_product_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1843,7 +2105,7 @@ func (x *User) ProtoReflect() protoreflect.Message { // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{38} + return file_product_proto_rawDescGZIP(), []int{44} } func (x *User) GetId() string { @@ -1871,7 +2133,7 @@ type NestedTypeA struct { func (x *NestedTypeA) Reset() { *x = NestedTypeA{} - mi := &file_product_proto_msgTypes[39] + mi := &file_product_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1883,7 +2145,7 @@ func (x *NestedTypeA) String() string { func (*NestedTypeA) ProtoMessage() {} func (x *NestedTypeA) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[39] + mi := &file_product_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1896,7 +2158,7 @@ func (x *NestedTypeA) ProtoReflect() protoreflect.Message { // Deprecated: Use NestedTypeA.ProtoReflect.Descriptor instead. func (*NestedTypeA) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{39} + return file_product_proto_rawDescGZIP(), []int{45} } func (x *NestedTypeA) GetId() string { @@ -1931,7 +2193,7 @@ type RecursiveType struct { func (x *RecursiveType) Reset() { *x = RecursiveType{} - mi := &file_product_proto_msgTypes[40] + mi := &file_product_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1943,7 +2205,7 @@ func (x *RecursiveType) String() string { func (*RecursiveType) ProtoMessage() {} func (x *RecursiveType) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[40] + mi := &file_product_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1956,7 +2218,7 @@ func (x *RecursiveType) ProtoReflect() protoreflect.Message { // Deprecated: Use RecursiveType.ProtoReflect.Descriptor instead. func (*RecursiveType) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{40} + return file_product_proto_rawDescGZIP(), []int{46} } func (x *RecursiveType) GetId() string { @@ -1992,7 +2254,7 @@ type TypeWithMultipleFilterFields struct { func (x *TypeWithMultipleFilterFields) Reset() { *x = TypeWithMultipleFilterFields{} - mi := &file_product_proto_msgTypes[41] + mi := &file_product_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2004,7 +2266,7 @@ func (x *TypeWithMultipleFilterFields) String() string { func (*TypeWithMultipleFilterFields) ProtoMessage() {} func (x *TypeWithMultipleFilterFields) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[41] + mi := &file_product_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2017,7 +2279,7 @@ func (x *TypeWithMultipleFilterFields) ProtoReflect() protoreflect.Message { // Deprecated: Use TypeWithMultipleFilterFields.ProtoReflect.Descriptor instead. func (*TypeWithMultipleFilterFields) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{41} + return file_product_proto_rawDescGZIP(), []int{47} } func (x *TypeWithMultipleFilterFields) GetId() string { @@ -2058,7 +2320,7 @@ type FilterTypeInput struct { func (x *FilterTypeInput) Reset() { *x = FilterTypeInput{} - mi := &file_product_proto_msgTypes[42] + mi := &file_product_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2070,7 +2332,7 @@ func (x *FilterTypeInput) String() string { func (*FilterTypeInput) ProtoMessage() {} func (x *FilterTypeInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[42] + mi := &file_product_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2083,7 +2345,7 @@ func (x *FilterTypeInput) ProtoReflect() protoreflect.Message { // Deprecated: Use FilterTypeInput.ProtoReflect.Descriptor instead. func (*FilterTypeInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{42} + return file_product_proto_rawDescGZIP(), []int{48} } func (x *FilterTypeInput) GetFilterField_1() string { @@ -2109,7 +2371,7 @@ type ComplexFilterTypeInput struct { func (x *ComplexFilterTypeInput) Reset() { *x = ComplexFilterTypeInput{} - mi := &file_product_proto_msgTypes[43] + mi := &file_product_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2121,7 +2383,7 @@ func (x *ComplexFilterTypeInput) String() string { func (*ComplexFilterTypeInput) ProtoMessage() {} func (x *ComplexFilterTypeInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[43] + mi := &file_product_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2134,7 +2396,7 @@ func (x *ComplexFilterTypeInput) ProtoReflect() protoreflect.Message { // Deprecated: Use ComplexFilterTypeInput.ProtoReflect.Descriptor instead. func (*ComplexFilterTypeInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{43} + return file_product_proto_rawDescGZIP(), []int{49} } func (x *ComplexFilterTypeInput) GetFilter() *FilterType { @@ -2154,7 +2416,7 @@ type TypeWithComplexFilterInput struct { func (x *TypeWithComplexFilterInput) Reset() { *x = TypeWithComplexFilterInput{} - mi := &file_product_proto_msgTypes[44] + mi := &file_product_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2166,7 +2428,7 @@ func (x *TypeWithComplexFilterInput) String() string { func (*TypeWithComplexFilterInput) ProtoMessage() {} func (x *TypeWithComplexFilterInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[44] + mi := &file_product_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2179,7 +2441,7 @@ func (x *TypeWithComplexFilterInput) ProtoReflect() protoreflect.Message { // Deprecated: Use TypeWithComplexFilterInput.ProtoReflect.Descriptor instead. func (*TypeWithComplexFilterInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{44} + return file_product_proto_rawDescGZIP(), []int{50} } func (x *TypeWithComplexFilterInput) GetId() string { @@ -2207,7 +2469,7 @@ type OrderInput struct { func (x *OrderInput) Reset() { *x = OrderInput{} - mi := &file_product_proto_msgTypes[45] + mi := &file_product_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2219,7 +2481,7 @@ func (x *OrderInput) String() string { func (*OrderInput) ProtoMessage() {} func (x *OrderInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[45] + mi := &file_product_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2232,7 +2494,7 @@ func (x *OrderInput) ProtoReflect() protoreflect.Message { // Deprecated: Use OrderInput.ProtoReflect.Descriptor instead. func (*OrderInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{45} + return file_product_proto_rawDescGZIP(), []int{51} } func (x *OrderInput) GetOrderId() string { @@ -2268,7 +2530,7 @@ type Order struct { func (x *Order) Reset() { *x = Order{} - mi := &file_product_proto_msgTypes[46] + mi := &file_product_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2280,7 +2542,7 @@ func (x *Order) String() string { func (*Order) ProtoMessage() {} func (x *Order) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[46] + mi := &file_product_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2293,7 +2555,7 @@ func (x *Order) ProtoReflect() protoreflect.Message { // Deprecated: Use Order.ProtoReflect.Descriptor instead. func (*Order) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{46} + return file_product_proto_rawDescGZIP(), []int{52} } func (x *Order) GetOrderId() string { @@ -2307,47 +2569,389 @@ func (x *Order) GetCustomerName() string { if x != nil { return x.CustomerName } - return "" + return "" +} + +func (x *Order) GetTotalItems() int32 { + if x != nil { + return x.TotalItems + } + return 0 +} + +func (x *Order) GetOrderLines() []*OrderLine { + if x != nil { + return x.OrderLines + } + return nil +} + +type Category struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Kind CategoryKind `protobuf:"varint,3,opt,name=kind,proto3,enum=productv1.CategoryKind" json:"kind,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Category) Reset() { + *x = Category{} + mi := &file_product_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Category) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Category) ProtoMessage() {} + +func (x *Category) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Category.ProtoReflect.Descriptor instead. +func (*Category) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{53} +} + +func (x *Category) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Category) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Category) GetKind() CategoryKind { + if x != nil { + return x.Kind + } + return CategoryKind_CATEGORY_KIND_UNSPECIFIED +} + +type CategoryFilter struct { + state protoimpl.MessageState `protogen:"open.v1"` + Category CategoryKind `protobuf:"varint,1,opt,name=category,proto3,enum=productv1.CategoryKind" json:"category,omitempty"` + Pagination *Pagination `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CategoryFilter) Reset() { + *x = CategoryFilter{} + mi := &file_product_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CategoryFilter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CategoryFilter) ProtoMessage() {} + +func (x *CategoryFilter) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CategoryFilter.ProtoReflect.Descriptor instead. +func (*CategoryFilter) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{54} +} + +func (x *CategoryFilter) GetCategory() CategoryKind { + if x != nil { + return x.Category + } + return CategoryKind_CATEGORY_KIND_UNSPECIFIED +} + +func (x *CategoryFilter) GetPagination() *Pagination { + if x != nil { + return x.Pagination + } + return nil +} + +type Animal struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Instance: + // + // *Animal_Cat + // *Animal_Dog + Instance isAnimal_Instance `protobuf_oneof:"instance"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Animal) Reset() { + *x = Animal{} + mi := &file_product_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Animal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Animal) ProtoMessage() {} + +func (x *Animal) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Animal.ProtoReflect.Descriptor instead. +func (*Animal) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{55} +} + +func (x *Animal) GetInstance() isAnimal_Instance { + if x != nil { + return x.Instance + } + return nil +} + +func (x *Animal) GetCat() *Cat { + if x != nil { + if x, ok := x.Instance.(*Animal_Cat); ok { + return x.Cat + } + } + return nil +} + +func (x *Animal) GetDog() *Dog { + if x != nil { + if x, ok := x.Instance.(*Animal_Dog); ok { + return x.Dog + } + } + return nil +} + +type isAnimal_Instance interface { + isAnimal_Instance() +} + +type Animal_Cat struct { + Cat *Cat `protobuf:"bytes,1,opt,name=cat,proto3,oneof"` +} + +type Animal_Dog struct { + Dog *Dog `protobuf:"bytes,2,opt,name=dog,proto3,oneof"` +} + +func (*Animal_Cat) isAnimal_Instance() {} + +func (*Animal_Dog) isAnimal_Instance() {} + +type SearchInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchInput) Reset() { + *x = SearchInput{} + mi := &file_product_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchInput) ProtoMessage() {} + +func (x *SearchInput) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchInput.ProtoReflect.Descriptor instead. +func (*SearchInput) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{56} +} + +func (x *SearchInput) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *SearchInput) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type SearchResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *SearchResult_Product + // *SearchResult_User + // *SearchResult_Category + Value isSearchResult_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchResult) Reset() { + *x = SearchResult{} + mi := &file_product_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResult) ProtoMessage() {} + +func (x *SearchResult) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResult.ProtoReflect.Descriptor instead. +func (*SearchResult) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{57} +} + +func (x *SearchResult) GetValue() isSearchResult_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *SearchResult) GetProduct() *Product { + if x != nil { + if x, ok := x.Value.(*SearchResult_Product); ok { + return x.Product + } + } + return nil } -func (x *Order) GetTotalItems() int32 { +func (x *SearchResult) GetUser() *User { if x != nil { - return x.TotalItems + if x, ok := x.Value.(*SearchResult_User); ok { + return x.User + } } - return 0 + return nil } -func (x *Order) GetOrderLines() []*OrderLine { +func (x *SearchResult) GetCategory() *Category { if x != nil { - return x.OrderLines + if x, ok := x.Value.(*SearchResult_Category); ok { + return x.Category + } } return nil } -type Category struct { +type isSearchResult_Value interface { + isSearchResult_Value() +} + +type SearchResult_Product struct { + Product *Product `protobuf:"bytes,1,opt,name=product,proto3,oneof"` +} + +type SearchResult_User struct { + User *User `protobuf:"bytes,2,opt,name=user,proto3,oneof"` +} + +type SearchResult_Category struct { + Category *Category `protobuf:"bytes,3,opt,name=category,proto3,oneof"` +} + +func (*SearchResult_Product) isSearchResult_Value() {} + +func (*SearchResult_User) isSearchResult_Value() {} + +func (*SearchResult_Category) isSearchResult_Value() {} + +type UserInput struct { state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Kind CategoryKind `protobuf:"varint,3,opt,name=kind,proto3,enum=productv1.CategoryKind" json:"kind,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Category) Reset() { - *x = Category{} - mi := &file_product_proto_msgTypes[47] +func (x *UserInput) Reset() { + *x = UserInput{} + mi := &file_product_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Category) String() string { +func (x *UserInput) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Category) ProtoMessage() {} +func (*UserInput) ProtoMessage() {} -func (x *Category) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[47] +func (x *UserInput) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2358,55 +2962,41 @@ func (x *Category) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Category.ProtoReflect.Descriptor instead. -func (*Category) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{47} -} - -func (x *Category) GetId() string { - if x != nil { - return x.Id - } - return "" +// Deprecated: Use UserInput.ProtoReflect.Descriptor instead. +func (*UserInput) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{58} } -func (x *Category) GetName() string { +func (x *UserInput) GetName() string { if x != nil { return x.Name } return "" } -func (x *Category) GetKind() CategoryKind { - if x != nil { - return x.Kind - } - return CategoryKind_CATEGORY_KIND_UNSPECIFIED -} - -type CategoryFilter struct { +type ActionInput struct { state protoimpl.MessageState `protogen:"open.v1"` - Category CategoryKind `protobuf:"varint,1,opt,name=category,proto3,enum=productv1.CategoryKind" json:"category,omitempty"` - Pagination *Pagination `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Payload string `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CategoryFilter) Reset() { - *x = CategoryFilter{} - mi := &file_product_proto_msgTypes[48] +func (x *ActionInput) Reset() { + *x = ActionInput{} + mi := &file_product_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CategoryFilter) String() string { +func (x *ActionInput) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CategoryFilter) ProtoMessage() {} +func (*ActionInput) ProtoMessage() {} -func (x *CategoryFilter) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[48] +func (x *ActionInput) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2417,51 +3007,51 @@ func (x *CategoryFilter) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CategoryFilter.ProtoReflect.Descriptor instead. -func (*CategoryFilter) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{48} +// Deprecated: Use ActionInput.ProtoReflect.Descriptor instead. +func (*ActionInput) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{59} } -func (x *CategoryFilter) GetCategory() CategoryKind { +func (x *ActionInput) GetType() string { if x != nil { - return x.Category + return x.Type } - return CategoryKind_CATEGORY_KIND_UNSPECIFIED + return "" } -func (x *CategoryFilter) GetPagination() *Pagination { +func (x *ActionInput) GetPayload() string { if x != nil { - return x.Pagination + return x.Payload } - return nil + return "" } -type Animal struct { +type ActionResult struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Instance: + // Types that are valid to be assigned to Value: // - // *Animal_Cat - // *Animal_Dog - Instance isAnimal_Instance `protobuf_oneof:"instance"` + // *ActionResult_ActionSuccess + // *ActionResult_ActionError + Value isActionResult_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Animal) Reset() { - *x = Animal{} - mi := &file_product_proto_msgTypes[49] +func (x *ActionResult) Reset() { + *x = ActionResult{} + mi := &file_product_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Animal) String() string { +func (x *ActionResult) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Animal) ProtoMessage() {} +func (*ActionResult) ProtoMessage() {} -func (x *Animal) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[49] +func (x *ActionResult) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2472,95 +3062,51 @@ func (x *Animal) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Animal.ProtoReflect.Descriptor instead. -func (*Animal) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{49} +// Deprecated: Use ActionResult.ProtoReflect.Descriptor instead. +func (*ActionResult) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{60} } -func (x *Animal) GetInstance() isAnimal_Instance { +func (x *ActionResult) GetValue() isActionResult_Value { if x != nil { - return x.Instance + return x.Value } return nil } -func (x *Animal) GetCat() *Cat { +func (x *ActionResult) GetActionSuccess() *ActionSuccess { if x != nil { - if x, ok := x.Instance.(*Animal_Cat); ok { - return x.Cat + if x, ok := x.Value.(*ActionResult_ActionSuccess); ok { + return x.ActionSuccess } } return nil } -func (x *Animal) GetDog() *Dog { +func (x *ActionResult) GetActionError() *ActionError { if x != nil { - if x, ok := x.Instance.(*Animal_Dog); ok { - return x.Dog + if x, ok := x.Value.(*ActionResult_ActionError); ok { + return x.ActionError } } return nil } -type isAnimal_Instance interface { - isAnimal_Instance() -} - -type Animal_Cat struct { - Cat *Cat `protobuf:"bytes,1,opt,name=cat,proto3,oneof"` -} - -type Animal_Dog struct { - Dog *Dog `protobuf:"bytes,2,opt,name=dog,proto3,oneof"` -} - -func (*Animal_Cat) isAnimal_Instance() {} - -func (*Animal_Dog) isAnimal_Instance() {} - -type UserInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UserInput) Reset() { - *x = UserInput{} - mi := &file_product_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +type isActionResult_Value interface { + isActionResult_Value() } -func (x *UserInput) String() string { - return protoimpl.X.MessageStringOf(x) +type ActionResult_ActionSuccess struct { + ActionSuccess *ActionSuccess `protobuf:"bytes,1,opt,name=action_success,json=actionSuccess,proto3,oneof"` } -func (*UserInput) ProtoMessage() {} - -func (x *UserInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[50] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) +type ActionResult_ActionError struct { + ActionError *ActionError `protobuf:"bytes,2,opt,name=action_error,json=actionError,proto3,oneof"` } -// Deprecated: Use UserInput.ProtoReflect.Descriptor instead. -func (*UserInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{50} -} +func (*ActionResult_ActionSuccess) isActionResult_Value() {} -func (x *UserInput) GetName() string { - if x != nil { - return x.Name - } - return "" -} +func (*ActionResult_ActionError) isActionResult_Value() {} type NestedTypeB struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2573,7 +3119,7 @@ type NestedTypeB struct { func (x *NestedTypeB) Reset() { *x = NestedTypeB{} - mi := &file_product_proto_msgTypes[51] + mi := &file_product_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2585,7 +3131,7 @@ func (x *NestedTypeB) String() string { func (*NestedTypeB) ProtoMessage() {} func (x *NestedTypeB) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[51] + mi := &file_product_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2598,7 +3144,7 @@ func (x *NestedTypeB) ProtoReflect() protoreflect.Message { // Deprecated: Use NestedTypeB.ProtoReflect.Descriptor instead. func (*NestedTypeB) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{51} + return file_product_proto_rawDescGZIP(), []int{61} } func (x *NestedTypeB) GetId() string { @@ -2632,7 +3178,7 @@ type NestedTypeC struct { func (x *NestedTypeC) Reset() { *x = NestedTypeC{} - mi := &file_product_proto_msgTypes[52] + mi := &file_product_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2644,7 +3190,7 @@ func (x *NestedTypeC) String() string { func (*NestedTypeC) ProtoMessage() {} func (x *NestedTypeC) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[52] + mi := &file_product_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2657,7 +3203,7 @@ func (x *NestedTypeC) ProtoReflect() protoreflect.Message { // Deprecated: Use NestedTypeC.ProtoReflect.Descriptor instead. func (*NestedTypeC) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{52} + return file_product_proto_rawDescGZIP(), []int{62} } func (x *NestedTypeC) GetId() string { @@ -2686,7 +3232,7 @@ type FilterType struct { func (x *FilterType) Reset() { *x = FilterType{} - mi := &file_product_proto_msgTypes[53] + mi := &file_product_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2698,7 +3244,7 @@ func (x *FilterType) String() string { func (*FilterType) ProtoMessage() {} func (x *FilterType) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[53] + mi := &file_product_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2711,7 +3257,7 @@ func (x *FilterType) ProtoReflect() protoreflect.Message { // Deprecated: Use FilterType.ProtoReflect.Descriptor instead. func (*FilterType) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{53} + return file_product_proto_rawDescGZIP(), []int{63} } func (x *FilterType) GetName() string { @@ -2752,7 +3298,7 @@ type Pagination struct { func (x *Pagination) Reset() { *x = Pagination{} - mi := &file_product_proto_msgTypes[54] + mi := &file_product_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2764,7 +3310,7 @@ func (x *Pagination) String() string { func (*Pagination) ProtoMessage() {} func (x *Pagination) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[54] + mi := &file_product_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2777,7 +3323,7 @@ func (x *Pagination) ProtoReflect() protoreflect.Message { // Deprecated: Use Pagination.ProtoReflect.Descriptor instead. func (*Pagination) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{54} + return file_product_proto_rawDescGZIP(), []int{64} } func (x *Pagination) GetPage() int32 { @@ -2805,7 +3351,7 @@ type OrderLineInput struct { func (x *OrderLineInput) Reset() { *x = OrderLineInput{} - mi := &file_product_proto_msgTypes[55] + mi := &file_product_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2817,7 +3363,7 @@ func (x *OrderLineInput) String() string { func (*OrderLineInput) ProtoMessage() {} func (x *OrderLineInput) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[55] + mi := &file_product_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2830,7 +3376,7 @@ func (x *OrderLineInput) ProtoReflect() protoreflect.Message { // Deprecated: Use OrderLineInput.ProtoReflect.Descriptor instead. func (*OrderLineInput) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{55} + return file_product_proto_rawDescGZIP(), []int{65} } func (x *OrderLineInput) GetProductId() string { @@ -2865,7 +3411,7 @@ type OrderLine struct { func (x *OrderLine) Reset() { *x = OrderLine{} - mi := &file_product_proto_msgTypes[56] + mi := &file_product_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2877,7 +3423,7 @@ func (x *OrderLine) String() string { func (*OrderLine) ProtoMessage() {} func (x *OrderLine) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[56] + mi := &file_product_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2890,7 +3436,7 @@ func (x *OrderLine) ProtoReflect() protoreflect.Message { // Deprecated: Use OrderLine.ProtoReflect.Descriptor instead. func (*OrderLine) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{56} + return file_product_proto_rawDescGZIP(), []int{66} } func (x *OrderLine) GetProductId() string { @@ -2926,7 +3472,7 @@ type Cat struct { func (x *Cat) Reset() { *x = Cat{} - mi := &file_product_proto_msgTypes[57] + mi := &file_product_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2938,7 +3484,7 @@ func (x *Cat) String() string { func (*Cat) ProtoMessage() {} func (x *Cat) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[57] + mi := &file_product_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2951,7 +3497,7 @@ func (x *Cat) ProtoReflect() protoreflect.Message { // Deprecated: Use Cat.ProtoReflect.Descriptor instead. func (*Cat) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{57} + return file_product_proto_rawDescGZIP(), []int{67} } func (x *Cat) GetId() string { @@ -2994,7 +3540,7 @@ type Dog struct { func (x *Dog) Reset() { *x = Dog{} - mi := &file_product_proto_msgTypes[58] + mi := &file_product_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3006,7 +3552,7 @@ func (x *Dog) String() string { func (*Dog) ProtoMessage() {} func (x *Dog) ProtoReflect() protoreflect.Message { - mi := &file_product_proto_msgTypes[58] + mi := &file_product_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3019,7 +3565,7 @@ func (x *Dog) ProtoReflect() protoreflect.Message { // Deprecated: Use Dog.ProtoReflect.Descriptor instead. func (*Dog) Descriptor() ([]byte, []int) { - return file_product_proto_rawDescGZIP(), []int{58} + return file_product_proto_rawDescGZIP(), []int{68} } func (x *Dog) GetId() string { @@ -3050,6 +3596,110 @@ func (x *Dog) GetBarkVolume() int32 { return 0 } +type ActionSuccess struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Timestamp string `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActionSuccess) Reset() { + *x = ActionSuccess{} + mi := &file_product_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActionSuccess) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActionSuccess) ProtoMessage() {} + +func (x *ActionSuccess) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActionSuccess.ProtoReflect.Descriptor instead. +func (*ActionSuccess) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{69} +} + +func (x *ActionSuccess) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ActionSuccess) GetTimestamp() string { + if x != nil { + return x.Timestamp + } + return "" +} + +type ActionError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActionError) Reset() { + *x = ActionError{} + mi := &file_product_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActionError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActionError) ProtoMessage() {} + +func (x *ActionError) ProtoReflect() protoreflect.Message { + mi := &file_product_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActionError.ProtoReflect.Descriptor instead. +func (*ActionError) Descriptor() ([]byte, []int) { + return file_product_proto_rawDescGZIP(), []int{70} +} + +func (x *ActionError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ActionError) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + var File_product_proto protoreflect.FileDescriptor const file_product_proto_rawDesc = "" + @@ -3121,12 +3771,23 @@ const file_product_proto_rawDesc = "" + "random_pet\x18\x01 \x01(\v2\x11.productv1.AnimalR\trandomPet\"\x15\n" + "\x13QueryAllPetsRequest\"D\n" + "\x14QueryAllPetsResponse\x12,\n" + - "\ball_pets\x18\x01 \x03(\v2\x11.productv1.AnimalR\aallPets\"G\n" + + "\ball_pets\x18\x01 \x03(\v2\x11.productv1.AnimalR\aallPets\"B\n" + + "\x12QuerySearchRequest\x12,\n" + + "\x05input\x18\x01 \x01(\v2\x16.productv1.SearchInputR\x05input\"F\n" + + "\x13QuerySearchResponse\x12/\n" + + "\x06search\x18\x01 \x03(\v2\x17.productv1.SearchResultR\x06search\" \n" + + "\x1eQueryRandomSearchResultRequest\"l\n" + + "\x1fQueryRandomSearchResultResponse\x12I\n" + + "\x14random_search_result\x18\x01 \x01(\v2\x17.productv1.SearchResultR\x12randomSearchResult\"G\n" + "\x19MutationCreateUserRequest\x12*\n" + "\x05input\x18\x01 \x01(\v2\x14.productv1.UserInputR\x05input\"N\n" + "\x1aMutationCreateUserResponse\x120\n" + "\vcreate_user\x18\x01 \x01(\v2\x0f.productv1.UserR\n" + - "createUser\"C\n" + + "createUser\"L\n" + + "\x1cMutationPerformActionRequest\x12,\n" + + "\x05input\x18\x01 \x01(\v2\x16.productv1.ActionInputR\x05input\"_\n" + + "\x1dMutationPerformActionResponse\x12>\n" + + "\x0eperform_action\x18\x01 \x01(\v2\x17.productv1.ActionResultR\rperformAction\"C\n" + "\aProduct\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + @@ -3184,9 +3845,24 @@ const file_product_proto_rawDesc = "" + "\x03cat\x18\x01 \x01(\v2\x0e.productv1.CatH\x00R\x03cat\x12\"\n" + "\x03dog\x18\x02 \x01(\v2\x0e.productv1.DogH\x00R\x03dogB\n" + "\n" + - "\binstance\"\x1f\n" + + "\binstance\"9\n" + + "\vSearchInput\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\"\xa1\x01\n" + + "\fSearchResult\x12.\n" + + "\aproduct\x18\x01 \x01(\v2\x12.productv1.ProductH\x00R\aproduct\x12%\n" + + "\x04user\x18\x02 \x01(\v2\x0f.productv1.UserH\x00R\x04user\x121\n" + + "\bcategory\x18\x03 \x01(\v2\x13.productv1.CategoryH\x00R\bcategoryB\a\n" + + "\x05value\"\x1f\n" + "\tUserInput\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"W\n" + + "\x04name\x18\x01 \x01(\tR\x04name\";\n" + + "\vActionInput\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x18\n" + + "\apayload\x18\x02 \x01(\tR\apayload\"\x97\x01\n" + + "\fActionResult\x12A\n" + + "\x0eaction_success\x18\x01 \x01(\v2\x18.productv1.ActionSuccessH\x00R\ractionSuccess\x12;\n" + + "\faction_error\x18\x02 \x01(\v2\x16.productv1.ActionErrorH\x00R\vactionErrorB\a\n" + + "\x05value\"W\n" + "\vNestedTypeB\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12$\n" + @@ -3227,17 +3903,24 @@ const file_product_proto_rawDesc = "" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + "\x04kind\x18\x03 \x01(\tR\x04kind\x12\x1f\n" + "\vbark_volume\x18\x04 \x01(\x05R\n" + - "barkVolume*\x9a\x01\n" + + "barkVolume\"G\n" + + "\rActionSuccess\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + + "\ttimestamp\x18\x02 \x01(\tR\ttimestamp\";\n" + + "\vActionError\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" + + "\x04code\x18\x02 \x01(\tR\x04code*\x9a\x01\n" + "\fCategoryKind\x12\x1d\n" + "\x19CATEGORY_KIND_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12CATEGORY_KIND_BOOK\x10\x01\x12\x1d\n" + "\x19CATEGORY_KIND_ELECTRONICS\x10\x02\x12\x1b\n" + "\x17CATEGORY_KIND_FURNITURE\x10\x03\x12\x17\n" + - "\x13CATEGORY_KIND_OTHER\x10\x042\xd9\r\n" + + "\x13CATEGORY_KIND_OTHER\x10\x042\x8b\x10\n" + "\x0eProductService\x12`\n" + "\x11LookupProductById\x12#.productv1.LookupProductByIdRequest\x1a$.productv1.LookupProductByIdResponse\"\x00\x12`\n" + "\x11LookupStorageById\x12#.productv1.LookupStorageByIdRequest\x1a$.productv1.LookupStorageByIdResponse\"\x00\x12c\n" + - "\x12MutationCreateUser\x12$.productv1.MutationCreateUserRequest\x1a%.productv1.MutationCreateUserResponse\"\x00\x12Q\n" + + "\x12MutationCreateUser\x12$.productv1.MutationCreateUserRequest\x1a%.productv1.MutationCreateUserResponse\"\x00\x12l\n" + + "\x15MutationPerformAction\x12'.productv1.MutationPerformActionRequest\x1a(.productv1.MutationPerformActionResponse\"\x00\x12Q\n" + "\fQueryAllPets\x12\x1e.productv1.QueryAllPetsRequest\x1a\x1f.productv1.QueryAllPetsResponse\"\x00\x12i\n" + "\x14QueryCalculateTotals\x12&.productv1.QueryCalculateTotalsRequest\x1a'.productv1.QueryCalculateTotalsResponse\"\x00\x12Z\n" + "\x0fQueryCategories\x12!.productv1.QueryCategoriesRequest\x1a\".productv1.QueryCategoriesResponse\"\x00\x12l\n" + @@ -3246,8 +3929,10 @@ const file_product_proto_rawDesc = "" + "\x16QueryComplexFilterType\x12(.productv1.QueryComplexFilterTypeRequest\x1a).productv1.QueryComplexFilterTypeResponse\"\x00\x12l\n" + "\x15QueryFilterCategories\x12'.productv1.QueryFilterCategoriesRequest\x1a(.productv1.QueryFilterCategoriesResponse\"\x00\x12Z\n" + "\x0fQueryNestedType\x12!.productv1.QueryNestedTypeRequest\x1a\".productv1.QueryNestedTypeResponse\"\x00\x12W\n" + - "\x0eQueryRandomPet\x12 .productv1.QueryRandomPetRequest\x1a!.productv1.QueryRandomPetResponse\"\x00\x12c\n" + - "\x12QueryRecursiveType\x12$.productv1.QueryRecursiveTypeRequest\x1a%.productv1.QueryRecursiveTypeResponse\"\x00\x12\x81\x01\n" + + "\x0eQueryRandomPet\x12 .productv1.QueryRandomPetRequest\x1a!.productv1.QueryRandomPetResponse\"\x00\x12r\n" + + "\x17QueryRandomSearchResult\x12).productv1.QueryRandomSearchResultRequest\x1a*.productv1.QueryRandomSearchResultResponse\"\x00\x12c\n" + + "\x12QueryRecursiveType\x12$.productv1.QueryRecursiveTypeRequest\x1a%.productv1.QueryRecursiveTypeResponse\"\x00\x12N\n" + + "\vQuerySearch\x12\x1d.productv1.QuerySearchRequest\x1a\x1e.productv1.QuerySearchResponse\"\x00\x12\x81\x01\n" + "\x1cQueryTypeFilterWithArguments\x12..productv1.QueryTypeFilterWithArgumentsRequest\x1a/.productv1.QueryTypeFilterWithArgumentsResponse\"\x00\x12\x90\x01\n" + "!QueryTypeWithMultipleFilterFields\x123.productv1.QueryTypeWithMultipleFilterFieldsRequest\x1a4.productv1.QueryTypeWithMultipleFilterFieldsResponse\"\x00\x12H\n" + "\tQueryUser\x12\x1b.productv1.QueryUserRequest\x1a\x1c.productv1.QueryUserResponse\"\x00\x12K\n" + @@ -3267,7 +3952,7 @@ func file_product_proto_rawDescGZIP() []byte { } var file_product_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_product_proto_msgTypes = make([]protoimpl.MessageInfo, 59) +var file_product_proto_msgTypes = make([]protoimpl.MessageInfo, 71) var file_product_proto_goTypes = []any{ (CategoryKind)(0), // 0: productv1.CategoryKind (*LookupProductByIdRequestKey)(nil), // 1: productv1.LookupProductByIdRequestKey @@ -3304,110 +3989,138 @@ var file_product_proto_goTypes = []any{ (*QueryRandomPetResponse)(nil), // 32: productv1.QueryRandomPetResponse (*QueryAllPetsRequest)(nil), // 33: productv1.QueryAllPetsRequest (*QueryAllPetsResponse)(nil), // 34: productv1.QueryAllPetsResponse - (*MutationCreateUserRequest)(nil), // 35: productv1.MutationCreateUserRequest - (*MutationCreateUserResponse)(nil), // 36: productv1.MutationCreateUserResponse - (*Product)(nil), // 37: productv1.Product - (*Storage)(nil), // 38: productv1.Storage - (*User)(nil), // 39: productv1.User - (*NestedTypeA)(nil), // 40: productv1.NestedTypeA - (*RecursiveType)(nil), // 41: productv1.RecursiveType - (*TypeWithMultipleFilterFields)(nil), // 42: productv1.TypeWithMultipleFilterFields - (*FilterTypeInput)(nil), // 43: productv1.FilterTypeInput - (*ComplexFilterTypeInput)(nil), // 44: productv1.ComplexFilterTypeInput - (*TypeWithComplexFilterInput)(nil), // 45: productv1.TypeWithComplexFilterInput - (*OrderInput)(nil), // 46: productv1.OrderInput - (*Order)(nil), // 47: productv1.Order - (*Category)(nil), // 48: productv1.Category - (*CategoryFilter)(nil), // 49: productv1.CategoryFilter - (*Animal)(nil), // 50: productv1.Animal - (*UserInput)(nil), // 51: productv1.UserInput - (*NestedTypeB)(nil), // 52: productv1.NestedTypeB - (*NestedTypeC)(nil), // 53: productv1.NestedTypeC - (*FilterType)(nil), // 54: productv1.FilterType - (*Pagination)(nil), // 55: productv1.Pagination - (*OrderLineInput)(nil), // 56: productv1.OrderLineInput - (*OrderLine)(nil), // 57: productv1.OrderLine - (*Cat)(nil), // 58: productv1.Cat - (*Dog)(nil), // 59: productv1.Dog + (*QuerySearchRequest)(nil), // 35: productv1.QuerySearchRequest + (*QuerySearchResponse)(nil), // 36: productv1.QuerySearchResponse + (*QueryRandomSearchResultRequest)(nil), // 37: productv1.QueryRandomSearchResultRequest + (*QueryRandomSearchResultResponse)(nil), // 38: productv1.QueryRandomSearchResultResponse + (*MutationCreateUserRequest)(nil), // 39: productv1.MutationCreateUserRequest + (*MutationCreateUserResponse)(nil), // 40: productv1.MutationCreateUserResponse + (*MutationPerformActionRequest)(nil), // 41: productv1.MutationPerformActionRequest + (*MutationPerformActionResponse)(nil), // 42: productv1.MutationPerformActionResponse + (*Product)(nil), // 43: productv1.Product + (*Storage)(nil), // 44: productv1.Storage + (*User)(nil), // 45: productv1.User + (*NestedTypeA)(nil), // 46: productv1.NestedTypeA + (*RecursiveType)(nil), // 47: productv1.RecursiveType + (*TypeWithMultipleFilterFields)(nil), // 48: productv1.TypeWithMultipleFilterFields + (*FilterTypeInput)(nil), // 49: productv1.FilterTypeInput + (*ComplexFilterTypeInput)(nil), // 50: productv1.ComplexFilterTypeInput + (*TypeWithComplexFilterInput)(nil), // 51: productv1.TypeWithComplexFilterInput + (*OrderInput)(nil), // 52: productv1.OrderInput + (*Order)(nil), // 53: productv1.Order + (*Category)(nil), // 54: productv1.Category + (*CategoryFilter)(nil), // 55: productv1.CategoryFilter + (*Animal)(nil), // 56: productv1.Animal + (*SearchInput)(nil), // 57: productv1.SearchInput + (*SearchResult)(nil), // 58: productv1.SearchResult + (*UserInput)(nil), // 59: productv1.UserInput + (*ActionInput)(nil), // 60: productv1.ActionInput + (*ActionResult)(nil), // 61: productv1.ActionResult + (*NestedTypeB)(nil), // 62: productv1.NestedTypeB + (*NestedTypeC)(nil), // 63: productv1.NestedTypeC + (*FilterType)(nil), // 64: productv1.FilterType + (*Pagination)(nil), // 65: productv1.Pagination + (*OrderLineInput)(nil), // 66: productv1.OrderLineInput + (*OrderLine)(nil), // 67: productv1.OrderLine + (*Cat)(nil), // 68: productv1.Cat + (*Dog)(nil), // 69: productv1.Dog + (*ActionSuccess)(nil), // 70: productv1.ActionSuccess + (*ActionError)(nil), // 71: productv1.ActionError } var file_product_proto_depIdxs = []int32{ 1, // 0: productv1.LookupProductByIdRequest.keys:type_name -> productv1.LookupProductByIdRequestKey - 37, // 1: productv1.LookupProductByIdResponse.result:type_name -> productv1.Product + 43, // 1: productv1.LookupProductByIdResponse.result:type_name -> productv1.Product 4, // 2: productv1.LookupStorageByIdRequest.keys:type_name -> productv1.LookupStorageByIdRequestKey - 38, // 3: productv1.LookupStorageByIdResponse.result:type_name -> productv1.Storage - 39, // 4: productv1.QueryUsersResponse.users:type_name -> productv1.User - 39, // 5: productv1.QueryUserResponse.user:type_name -> productv1.User - 40, // 6: productv1.QueryNestedTypeResponse.nested_type:type_name -> productv1.NestedTypeA - 41, // 7: productv1.QueryRecursiveTypeResponse.recursive_type:type_name -> productv1.RecursiveType - 42, // 8: productv1.QueryTypeFilterWithArgumentsResponse.type_filter_with_arguments:type_name -> productv1.TypeWithMultipleFilterFields - 43, // 9: productv1.QueryTypeWithMultipleFilterFieldsRequest.filter:type_name -> productv1.FilterTypeInput - 42, // 10: productv1.QueryTypeWithMultipleFilterFieldsResponse.type_with_multiple_filter_fields:type_name -> productv1.TypeWithMultipleFilterFields - 44, // 11: productv1.QueryComplexFilterTypeRequest.filter:type_name -> productv1.ComplexFilterTypeInput - 45, // 12: productv1.QueryComplexFilterTypeResponse.complex_filter_type:type_name -> productv1.TypeWithComplexFilterInput - 46, // 13: productv1.QueryCalculateTotalsRequest.orders:type_name -> productv1.OrderInput - 47, // 14: productv1.QueryCalculateTotalsResponse.calculate_totals:type_name -> productv1.Order - 48, // 15: productv1.QueryCategoriesResponse.categories:type_name -> productv1.Category + 44, // 3: productv1.LookupStorageByIdResponse.result:type_name -> productv1.Storage + 45, // 4: productv1.QueryUsersResponse.users:type_name -> productv1.User + 45, // 5: productv1.QueryUserResponse.user:type_name -> productv1.User + 46, // 6: productv1.QueryNestedTypeResponse.nested_type:type_name -> productv1.NestedTypeA + 47, // 7: productv1.QueryRecursiveTypeResponse.recursive_type:type_name -> productv1.RecursiveType + 48, // 8: productv1.QueryTypeFilterWithArgumentsResponse.type_filter_with_arguments:type_name -> productv1.TypeWithMultipleFilterFields + 49, // 9: productv1.QueryTypeWithMultipleFilterFieldsRequest.filter:type_name -> productv1.FilterTypeInput + 48, // 10: productv1.QueryTypeWithMultipleFilterFieldsResponse.type_with_multiple_filter_fields:type_name -> productv1.TypeWithMultipleFilterFields + 50, // 11: productv1.QueryComplexFilterTypeRequest.filter:type_name -> productv1.ComplexFilterTypeInput + 51, // 12: productv1.QueryComplexFilterTypeResponse.complex_filter_type:type_name -> productv1.TypeWithComplexFilterInput + 52, // 13: productv1.QueryCalculateTotalsRequest.orders:type_name -> productv1.OrderInput + 53, // 14: productv1.QueryCalculateTotalsResponse.calculate_totals:type_name -> productv1.Order + 54, // 15: productv1.QueryCategoriesResponse.categories:type_name -> productv1.Category 0, // 16: productv1.QueryCategoriesByKindRequest.kind:type_name -> productv1.CategoryKind - 48, // 17: productv1.QueryCategoriesByKindResponse.categories_by_kind:type_name -> productv1.Category + 54, // 17: productv1.QueryCategoriesByKindResponse.categories_by_kind:type_name -> productv1.Category 0, // 18: productv1.QueryCategoriesByKindsRequest.kinds:type_name -> productv1.CategoryKind - 48, // 19: productv1.QueryCategoriesByKindsResponse.categories_by_kinds:type_name -> productv1.Category - 49, // 20: productv1.QueryFilterCategoriesRequest.filter:type_name -> productv1.CategoryFilter - 48, // 21: productv1.QueryFilterCategoriesResponse.filter_categories:type_name -> productv1.Category - 50, // 22: productv1.QueryRandomPetResponse.random_pet:type_name -> productv1.Animal - 50, // 23: productv1.QueryAllPetsResponse.all_pets:type_name -> productv1.Animal - 51, // 24: productv1.MutationCreateUserRequest.input:type_name -> productv1.UserInput - 39, // 25: productv1.MutationCreateUserResponse.create_user:type_name -> productv1.User - 52, // 26: productv1.NestedTypeA.b:type_name -> productv1.NestedTypeB - 41, // 27: productv1.RecursiveType.recursive_type:type_name -> productv1.RecursiveType - 54, // 28: productv1.ComplexFilterTypeInput.filter:type_name -> productv1.FilterType - 56, // 29: productv1.OrderInput.lines:type_name -> productv1.OrderLineInput - 57, // 30: productv1.Order.order_lines:type_name -> productv1.OrderLine - 0, // 31: productv1.Category.kind:type_name -> productv1.CategoryKind - 0, // 32: productv1.CategoryFilter.category:type_name -> productv1.CategoryKind - 55, // 33: productv1.CategoryFilter.pagination:type_name -> productv1.Pagination - 58, // 34: productv1.Animal.cat:type_name -> productv1.Cat - 59, // 35: productv1.Animal.dog:type_name -> productv1.Dog - 53, // 36: productv1.NestedTypeB.c:type_name -> productv1.NestedTypeC - 55, // 37: productv1.FilterType.pagination:type_name -> productv1.Pagination - 2, // 38: productv1.ProductService.LookupProductById:input_type -> productv1.LookupProductByIdRequest - 5, // 39: productv1.ProductService.LookupStorageById:input_type -> productv1.LookupStorageByIdRequest - 35, // 40: productv1.ProductService.MutationCreateUser:input_type -> productv1.MutationCreateUserRequest - 33, // 41: productv1.ProductService.QueryAllPets:input_type -> productv1.QueryAllPetsRequest - 21, // 42: productv1.ProductService.QueryCalculateTotals:input_type -> productv1.QueryCalculateTotalsRequest - 23, // 43: productv1.ProductService.QueryCategories:input_type -> productv1.QueryCategoriesRequest - 25, // 44: productv1.ProductService.QueryCategoriesByKind:input_type -> productv1.QueryCategoriesByKindRequest - 27, // 45: productv1.ProductService.QueryCategoriesByKinds:input_type -> productv1.QueryCategoriesByKindsRequest - 19, // 46: productv1.ProductService.QueryComplexFilterType:input_type -> productv1.QueryComplexFilterTypeRequest - 29, // 47: productv1.ProductService.QueryFilterCategories:input_type -> productv1.QueryFilterCategoriesRequest - 11, // 48: productv1.ProductService.QueryNestedType:input_type -> productv1.QueryNestedTypeRequest - 31, // 49: productv1.ProductService.QueryRandomPet:input_type -> productv1.QueryRandomPetRequest - 13, // 50: productv1.ProductService.QueryRecursiveType:input_type -> productv1.QueryRecursiveTypeRequest - 15, // 51: productv1.ProductService.QueryTypeFilterWithArguments:input_type -> productv1.QueryTypeFilterWithArgumentsRequest - 17, // 52: productv1.ProductService.QueryTypeWithMultipleFilterFields:input_type -> productv1.QueryTypeWithMultipleFilterFieldsRequest - 9, // 53: productv1.ProductService.QueryUser:input_type -> productv1.QueryUserRequest - 7, // 54: productv1.ProductService.QueryUsers:input_type -> productv1.QueryUsersRequest - 3, // 55: productv1.ProductService.LookupProductById:output_type -> productv1.LookupProductByIdResponse - 6, // 56: productv1.ProductService.LookupStorageById:output_type -> productv1.LookupStorageByIdResponse - 36, // 57: productv1.ProductService.MutationCreateUser:output_type -> productv1.MutationCreateUserResponse - 34, // 58: productv1.ProductService.QueryAllPets:output_type -> productv1.QueryAllPetsResponse - 22, // 59: productv1.ProductService.QueryCalculateTotals:output_type -> productv1.QueryCalculateTotalsResponse - 24, // 60: productv1.ProductService.QueryCategories:output_type -> productv1.QueryCategoriesResponse - 26, // 61: productv1.ProductService.QueryCategoriesByKind:output_type -> productv1.QueryCategoriesByKindResponse - 28, // 62: productv1.ProductService.QueryCategoriesByKinds:output_type -> productv1.QueryCategoriesByKindsResponse - 20, // 63: productv1.ProductService.QueryComplexFilterType:output_type -> productv1.QueryComplexFilterTypeResponse - 30, // 64: productv1.ProductService.QueryFilterCategories:output_type -> productv1.QueryFilterCategoriesResponse - 12, // 65: productv1.ProductService.QueryNestedType:output_type -> productv1.QueryNestedTypeResponse - 32, // 66: productv1.ProductService.QueryRandomPet:output_type -> productv1.QueryRandomPetResponse - 14, // 67: productv1.ProductService.QueryRecursiveType:output_type -> productv1.QueryRecursiveTypeResponse - 16, // 68: productv1.ProductService.QueryTypeFilterWithArguments:output_type -> productv1.QueryTypeFilterWithArgumentsResponse - 18, // 69: productv1.ProductService.QueryTypeWithMultipleFilterFields:output_type -> productv1.QueryTypeWithMultipleFilterFieldsResponse - 10, // 70: productv1.ProductService.QueryUser:output_type -> productv1.QueryUserResponse - 8, // 71: productv1.ProductService.QueryUsers:output_type -> productv1.QueryUsersResponse - 55, // [55:72] is the sub-list for method output_type - 38, // [38:55] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name + 54, // 19: productv1.QueryCategoriesByKindsResponse.categories_by_kinds:type_name -> productv1.Category + 55, // 20: productv1.QueryFilterCategoriesRequest.filter:type_name -> productv1.CategoryFilter + 54, // 21: productv1.QueryFilterCategoriesResponse.filter_categories:type_name -> productv1.Category + 56, // 22: productv1.QueryRandomPetResponse.random_pet:type_name -> productv1.Animal + 56, // 23: productv1.QueryAllPetsResponse.all_pets:type_name -> productv1.Animal + 57, // 24: productv1.QuerySearchRequest.input:type_name -> productv1.SearchInput + 58, // 25: productv1.QuerySearchResponse.search:type_name -> productv1.SearchResult + 58, // 26: productv1.QueryRandomSearchResultResponse.random_search_result:type_name -> productv1.SearchResult + 59, // 27: productv1.MutationCreateUserRequest.input:type_name -> productv1.UserInput + 45, // 28: productv1.MutationCreateUserResponse.create_user:type_name -> productv1.User + 60, // 29: productv1.MutationPerformActionRequest.input:type_name -> productv1.ActionInput + 61, // 30: productv1.MutationPerformActionResponse.perform_action:type_name -> productv1.ActionResult + 62, // 31: productv1.NestedTypeA.b:type_name -> productv1.NestedTypeB + 47, // 32: productv1.RecursiveType.recursive_type:type_name -> productv1.RecursiveType + 64, // 33: productv1.ComplexFilterTypeInput.filter:type_name -> productv1.FilterType + 66, // 34: productv1.OrderInput.lines:type_name -> productv1.OrderLineInput + 67, // 35: productv1.Order.order_lines:type_name -> productv1.OrderLine + 0, // 36: productv1.Category.kind:type_name -> productv1.CategoryKind + 0, // 37: productv1.CategoryFilter.category:type_name -> productv1.CategoryKind + 65, // 38: productv1.CategoryFilter.pagination:type_name -> productv1.Pagination + 68, // 39: productv1.Animal.cat:type_name -> productv1.Cat + 69, // 40: productv1.Animal.dog:type_name -> productv1.Dog + 43, // 41: productv1.SearchResult.product:type_name -> productv1.Product + 45, // 42: productv1.SearchResult.user:type_name -> productv1.User + 54, // 43: productv1.SearchResult.category:type_name -> productv1.Category + 70, // 44: productv1.ActionResult.action_success:type_name -> productv1.ActionSuccess + 71, // 45: productv1.ActionResult.action_error:type_name -> productv1.ActionError + 63, // 46: productv1.NestedTypeB.c:type_name -> productv1.NestedTypeC + 65, // 47: productv1.FilterType.pagination:type_name -> productv1.Pagination + 2, // 48: productv1.ProductService.LookupProductById:input_type -> productv1.LookupProductByIdRequest + 5, // 49: productv1.ProductService.LookupStorageById:input_type -> productv1.LookupStorageByIdRequest + 39, // 50: productv1.ProductService.MutationCreateUser:input_type -> productv1.MutationCreateUserRequest + 41, // 51: productv1.ProductService.MutationPerformAction:input_type -> productv1.MutationPerformActionRequest + 33, // 52: productv1.ProductService.QueryAllPets:input_type -> productv1.QueryAllPetsRequest + 21, // 53: productv1.ProductService.QueryCalculateTotals:input_type -> productv1.QueryCalculateTotalsRequest + 23, // 54: productv1.ProductService.QueryCategories:input_type -> productv1.QueryCategoriesRequest + 25, // 55: productv1.ProductService.QueryCategoriesByKind:input_type -> productv1.QueryCategoriesByKindRequest + 27, // 56: productv1.ProductService.QueryCategoriesByKinds:input_type -> productv1.QueryCategoriesByKindsRequest + 19, // 57: productv1.ProductService.QueryComplexFilterType:input_type -> productv1.QueryComplexFilterTypeRequest + 29, // 58: productv1.ProductService.QueryFilterCategories:input_type -> productv1.QueryFilterCategoriesRequest + 11, // 59: productv1.ProductService.QueryNestedType:input_type -> productv1.QueryNestedTypeRequest + 31, // 60: productv1.ProductService.QueryRandomPet:input_type -> productv1.QueryRandomPetRequest + 37, // 61: productv1.ProductService.QueryRandomSearchResult:input_type -> productv1.QueryRandomSearchResultRequest + 13, // 62: productv1.ProductService.QueryRecursiveType:input_type -> productv1.QueryRecursiveTypeRequest + 35, // 63: productv1.ProductService.QuerySearch:input_type -> productv1.QuerySearchRequest + 15, // 64: productv1.ProductService.QueryTypeFilterWithArguments:input_type -> productv1.QueryTypeFilterWithArgumentsRequest + 17, // 65: productv1.ProductService.QueryTypeWithMultipleFilterFields:input_type -> productv1.QueryTypeWithMultipleFilterFieldsRequest + 9, // 66: productv1.ProductService.QueryUser:input_type -> productv1.QueryUserRequest + 7, // 67: productv1.ProductService.QueryUsers:input_type -> productv1.QueryUsersRequest + 3, // 68: productv1.ProductService.LookupProductById:output_type -> productv1.LookupProductByIdResponse + 6, // 69: productv1.ProductService.LookupStorageById:output_type -> productv1.LookupStorageByIdResponse + 40, // 70: productv1.ProductService.MutationCreateUser:output_type -> productv1.MutationCreateUserResponse + 42, // 71: productv1.ProductService.MutationPerformAction:output_type -> productv1.MutationPerformActionResponse + 34, // 72: productv1.ProductService.QueryAllPets:output_type -> productv1.QueryAllPetsResponse + 22, // 73: productv1.ProductService.QueryCalculateTotals:output_type -> productv1.QueryCalculateTotalsResponse + 24, // 74: productv1.ProductService.QueryCategories:output_type -> productv1.QueryCategoriesResponse + 26, // 75: productv1.ProductService.QueryCategoriesByKind:output_type -> productv1.QueryCategoriesByKindResponse + 28, // 76: productv1.ProductService.QueryCategoriesByKinds:output_type -> productv1.QueryCategoriesByKindsResponse + 20, // 77: productv1.ProductService.QueryComplexFilterType:output_type -> productv1.QueryComplexFilterTypeResponse + 30, // 78: productv1.ProductService.QueryFilterCategories:output_type -> productv1.QueryFilterCategoriesResponse + 12, // 79: productv1.ProductService.QueryNestedType:output_type -> productv1.QueryNestedTypeResponse + 32, // 80: productv1.ProductService.QueryRandomPet:output_type -> productv1.QueryRandomPetResponse + 38, // 81: productv1.ProductService.QueryRandomSearchResult:output_type -> productv1.QueryRandomSearchResultResponse + 14, // 82: productv1.ProductService.QueryRecursiveType:output_type -> productv1.QueryRecursiveTypeResponse + 36, // 83: productv1.ProductService.QuerySearch:output_type -> productv1.QuerySearchResponse + 16, // 84: productv1.ProductService.QueryTypeFilterWithArguments:output_type -> productv1.QueryTypeFilterWithArgumentsResponse + 18, // 85: productv1.ProductService.QueryTypeWithMultipleFilterFields:output_type -> productv1.QueryTypeWithMultipleFilterFieldsResponse + 10, // 86: productv1.ProductService.QueryUser:output_type -> productv1.QueryUserResponse + 8, // 87: productv1.ProductService.QueryUsers:output_type -> productv1.QueryUsersResponse + 68, // [68:88] is the sub-list for method output_type + 48, // [48:68] is the sub-list for method input_type + 48, // [48:48] is the sub-list for extension type_name + 48, // [48:48] is the sub-list for extension extendee + 0, // [0:48] is the sub-list for field type_name } func init() { file_product_proto_init() } @@ -3415,17 +4128,26 @@ func file_product_proto_init() { if File_product_proto != nil { return } - file_product_proto_msgTypes[49].OneofWrappers = []any{ + file_product_proto_msgTypes[55].OneofWrappers = []any{ (*Animal_Cat)(nil), (*Animal_Dog)(nil), } + file_product_proto_msgTypes[57].OneofWrappers = []any{ + (*SearchResult_Product)(nil), + (*SearchResult_User)(nil), + (*SearchResult_Category)(nil), + } + file_product_proto_msgTypes[60].OneofWrappers = []any{ + (*ActionResult_ActionSuccess)(nil), + (*ActionResult_ActionError)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_product_proto_rawDesc), len(file_product_proto_rawDesc)), NumEnums: 1, - NumMessages: 59, + NumMessages: 71, NumExtensions: 0, NumServices: 1, }, diff --git a/v2/pkg/grpctest/productv1/product_grpc.pb.go b/v2/pkg/grpctest/productv1/product_grpc.pb.go index 1c22e64fcc..0fe45e5eb3 100644 --- a/v2/pkg/grpctest/productv1/product_grpc.pb.go +++ b/v2/pkg/grpctest/productv1/product_grpc.pb.go @@ -22,6 +22,7 @@ const ( ProductService_LookupProductById_FullMethodName = "/productv1.ProductService/LookupProductById" ProductService_LookupStorageById_FullMethodName = "/productv1.ProductService/LookupStorageById" ProductService_MutationCreateUser_FullMethodName = "/productv1.ProductService/MutationCreateUser" + ProductService_MutationPerformAction_FullMethodName = "/productv1.ProductService/MutationPerformAction" ProductService_QueryAllPets_FullMethodName = "/productv1.ProductService/QueryAllPets" ProductService_QueryCalculateTotals_FullMethodName = "/productv1.ProductService/QueryCalculateTotals" ProductService_QueryCategories_FullMethodName = "/productv1.ProductService/QueryCategories" @@ -31,7 +32,9 @@ const ( ProductService_QueryFilterCategories_FullMethodName = "/productv1.ProductService/QueryFilterCategories" ProductService_QueryNestedType_FullMethodName = "/productv1.ProductService/QueryNestedType" ProductService_QueryRandomPet_FullMethodName = "/productv1.ProductService/QueryRandomPet" + ProductService_QueryRandomSearchResult_FullMethodName = "/productv1.ProductService/QueryRandomSearchResult" ProductService_QueryRecursiveType_FullMethodName = "/productv1.ProductService/QueryRecursiveType" + ProductService_QuerySearch_FullMethodName = "/productv1.ProductService/QuerySearch" ProductService_QueryTypeFilterWithArguments_FullMethodName = "/productv1.ProductService/QueryTypeFilterWithArguments" ProductService_QueryTypeWithMultipleFilterFields_FullMethodName = "/productv1.ProductService/QueryTypeWithMultipleFilterFields" ProductService_QueryUser_FullMethodName = "/productv1.ProductService/QueryUser" @@ -49,6 +52,7 @@ type ProductServiceClient interface { // Lookup Storage entity by id LookupStorageById(ctx context.Context, in *LookupStorageByIdRequest, opts ...grpc.CallOption) (*LookupStorageByIdResponse, error) MutationCreateUser(ctx context.Context, in *MutationCreateUserRequest, opts ...grpc.CallOption) (*MutationCreateUserResponse, error) + MutationPerformAction(ctx context.Context, in *MutationPerformActionRequest, opts ...grpc.CallOption) (*MutationPerformActionResponse, error) QueryAllPets(ctx context.Context, in *QueryAllPetsRequest, opts ...grpc.CallOption) (*QueryAllPetsResponse, error) QueryCalculateTotals(ctx context.Context, in *QueryCalculateTotalsRequest, opts ...grpc.CallOption) (*QueryCalculateTotalsResponse, error) QueryCategories(ctx context.Context, in *QueryCategoriesRequest, opts ...grpc.CallOption) (*QueryCategoriesResponse, error) @@ -58,7 +62,9 @@ type ProductServiceClient interface { QueryFilterCategories(ctx context.Context, in *QueryFilterCategoriesRequest, opts ...grpc.CallOption) (*QueryFilterCategoriesResponse, error) QueryNestedType(ctx context.Context, in *QueryNestedTypeRequest, opts ...grpc.CallOption) (*QueryNestedTypeResponse, error) QueryRandomPet(ctx context.Context, in *QueryRandomPetRequest, opts ...grpc.CallOption) (*QueryRandomPetResponse, error) + QueryRandomSearchResult(ctx context.Context, in *QueryRandomSearchResultRequest, opts ...grpc.CallOption) (*QueryRandomSearchResultResponse, error) QueryRecursiveType(ctx context.Context, in *QueryRecursiveTypeRequest, opts ...grpc.CallOption) (*QueryRecursiveTypeResponse, error) + QuerySearch(ctx context.Context, in *QuerySearchRequest, opts ...grpc.CallOption) (*QuerySearchResponse, error) QueryTypeFilterWithArguments(ctx context.Context, in *QueryTypeFilterWithArgumentsRequest, opts ...grpc.CallOption) (*QueryTypeFilterWithArgumentsResponse, error) QueryTypeWithMultipleFilterFields(ctx context.Context, in *QueryTypeWithMultipleFilterFieldsRequest, opts ...grpc.CallOption) (*QueryTypeWithMultipleFilterFieldsResponse, error) QueryUser(ctx context.Context, in *QueryUserRequest, opts ...grpc.CallOption) (*QueryUserResponse, error) @@ -103,6 +109,16 @@ func (c *productServiceClient) MutationCreateUser(ctx context.Context, in *Mutat return out, nil } +func (c *productServiceClient) MutationPerformAction(ctx context.Context, in *MutationPerformActionRequest, opts ...grpc.CallOption) (*MutationPerformActionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MutationPerformActionResponse) + err := c.cc.Invoke(ctx, ProductService_MutationPerformAction_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *productServiceClient) QueryAllPets(ctx context.Context, in *QueryAllPetsRequest, opts ...grpc.CallOption) (*QueryAllPetsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(QueryAllPetsResponse) @@ -193,6 +209,16 @@ func (c *productServiceClient) QueryRandomPet(ctx context.Context, in *QueryRand return out, nil } +func (c *productServiceClient) QueryRandomSearchResult(ctx context.Context, in *QueryRandomSearchResultRequest, opts ...grpc.CallOption) (*QueryRandomSearchResultResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueryRandomSearchResultResponse) + err := c.cc.Invoke(ctx, ProductService_QueryRandomSearchResult_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *productServiceClient) QueryRecursiveType(ctx context.Context, in *QueryRecursiveTypeRequest, opts ...grpc.CallOption) (*QueryRecursiveTypeResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(QueryRecursiveTypeResponse) @@ -203,6 +229,16 @@ func (c *productServiceClient) QueryRecursiveType(ctx context.Context, in *Query return out, nil } +func (c *productServiceClient) QuerySearch(ctx context.Context, in *QuerySearchRequest, opts ...grpc.CallOption) (*QuerySearchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QuerySearchResponse) + err := c.cc.Invoke(ctx, ProductService_QuerySearch_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *productServiceClient) QueryTypeFilterWithArguments(ctx context.Context, in *QueryTypeFilterWithArgumentsRequest, opts ...grpc.CallOption) (*QueryTypeFilterWithArgumentsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(QueryTypeFilterWithArgumentsResponse) @@ -254,6 +290,7 @@ type ProductServiceServer interface { // Lookup Storage entity by id LookupStorageById(context.Context, *LookupStorageByIdRequest) (*LookupStorageByIdResponse, error) MutationCreateUser(context.Context, *MutationCreateUserRequest) (*MutationCreateUserResponse, error) + MutationPerformAction(context.Context, *MutationPerformActionRequest) (*MutationPerformActionResponse, error) QueryAllPets(context.Context, *QueryAllPetsRequest) (*QueryAllPetsResponse, error) QueryCalculateTotals(context.Context, *QueryCalculateTotalsRequest) (*QueryCalculateTotalsResponse, error) QueryCategories(context.Context, *QueryCategoriesRequest) (*QueryCategoriesResponse, error) @@ -263,7 +300,9 @@ type ProductServiceServer interface { QueryFilterCategories(context.Context, *QueryFilterCategoriesRequest) (*QueryFilterCategoriesResponse, error) QueryNestedType(context.Context, *QueryNestedTypeRequest) (*QueryNestedTypeResponse, error) QueryRandomPet(context.Context, *QueryRandomPetRequest) (*QueryRandomPetResponse, error) + QueryRandomSearchResult(context.Context, *QueryRandomSearchResultRequest) (*QueryRandomSearchResultResponse, error) QueryRecursiveType(context.Context, *QueryRecursiveTypeRequest) (*QueryRecursiveTypeResponse, error) + QuerySearch(context.Context, *QuerySearchRequest) (*QuerySearchResponse, error) QueryTypeFilterWithArguments(context.Context, *QueryTypeFilterWithArgumentsRequest) (*QueryTypeFilterWithArgumentsResponse, error) QueryTypeWithMultipleFilterFields(context.Context, *QueryTypeWithMultipleFilterFieldsRequest) (*QueryTypeWithMultipleFilterFieldsResponse, error) QueryUser(context.Context, *QueryUserRequest) (*QueryUserResponse, error) @@ -287,6 +326,9 @@ func (UnimplementedProductServiceServer) LookupStorageById(context.Context, *Loo func (UnimplementedProductServiceServer) MutationCreateUser(context.Context, *MutationCreateUserRequest) (*MutationCreateUserResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method MutationCreateUser not implemented") } +func (UnimplementedProductServiceServer) MutationPerformAction(context.Context, *MutationPerformActionRequest) (*MutationPerformActionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method MutationPerformAction not implemented") +} func (UnimplementedProductServiceServer) QueryAllPets(context.Context, *QueryAllPetsRequest) (*QueryAllPetsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method QueryAllPets not implemented") } @@ -314,9 +356,15 @@ func (UnimplementedProductServiceServer) QueryNestedType(context.Context, *Query func (UnimplementedProductServiceServer) QueryRandomPet(context.Context, *QueryRandomPetRequest) (*QueryRandomPetResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method QueryRandomPet not implemented") } +func (UnimplementedProductServiceServer) QueryRandomSearchResult(context.Context, *QueryRandomSearchResultRequest) (*QueryRandomSearchResultResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryRandomSearchResult not implemented") +} func (UnimplementedProductServiceServer) QueryRecursiveType(context.Context, *QueryRecursiveTypeRequest) (*QueryRecursiveTypeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method QueryRecursiveType not implemented") } +func (UnimplementedProductServiceServer) QuerySearch(context.Context, *QuerySearchRequest) (*QuerySearchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QuerySearch not implemented") +} func (UnimplementedProductServiceServer) QueryTypeFilterWithArguments(context.Context, *QueryTypeFilterWithArgumentsRequest) (*QueryTypeFilterWithArgumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method QueryTypeFilterWithArguments not implemented") } @@ -404,6 +452,24 @@ func _ProductService_MutationCreateUser_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _ProductService_MutationPerformAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationPerformActionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProductServiceServer).MutationPerformAction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProductService_MutationPerformAction_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProductServiceServer).MutationPerformAction(ctx, req.(*MutationPerformActionRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ProductService_QueryAllPets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryAllPetsRequest) if err := dec(in); err != nil { @@ -566,6 +632,24 @@ func _ProductService_QueryRandomPet_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _ProductService_QueryRandomSearchResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRandomSearchResultRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProductServiceServer).QueryRandomSearchResult(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProductService_QueryRandomSearchResult_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProductServiceServer).QueryRandomSearchResult(ctx, req.(*QueryRandomSearchResultRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ProductService_QueryRecursiveType_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryRecursiveTypeRequest) if err := dec(in); err != nil { @@ -584,6 +668,24 @@ func _ProductService_QueryRecursiveType_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _ProductService_QuerySearch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuerySearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProductServiceServer).QuerySearch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProductService_QuerySearch_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProductServiceServer).QuerySearch(ctx, req.(*QuerySearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ProductService_QueryTypeFilterWithArguments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryTypeFilterWithArgumentsRequest) if err := dec(in); err != nil { @@ -675,6 +777,10 @@ var ProductService_ServiceDesc = grpc.ServiceDesc{ MethodName: "MutationCreateUser", Handler: _ProductService_MutationCreateUser_Handler, }, + { + MethodName: "MutationPerformAction", + Handler: _ProductService_MutationPerformAction_Handler, + }, { MethodName: "QueryAllPets", Handler: _ProductService_QueryAllPets_Handler, @@ -711,10 +817,18 @@ var ProductService_ServiceDesc = grpc.ServiceDesc{ MethodName: "QueryRandomPet", Handler: _ProductService_QueryRandomPet_Handler, }, + { + MethodName: "QueryRandomSearchResult", + Handler: _ProductService_QueryRandomSearchResult_Handler, + }, { MethodName: "QueryRecursiveType", Handler: _ProductService_QueryRecursiveType_Handler, }, + { + MethodName: "QuerySearch", + Handler: _ProductService_QuerySearch_Handler, + }, { MethodName: "QueryTypeFilterWithArguments", Handler: _ProductService_QueryTypeFilterWithArguments_Handler, diff --git a/v2/pkg/grpctest/testdata/products.graphqls b/v2/pkg/grpctest/testdata/products.graphqls index 224bff06f6..421e376b32 100644 --- a/v2/pkg/grpctest/testdata/products.graphqls +++ b/v2/pkg/grpctest/testdata/products.graphqls @@ -135,6 +135,35 @@ type Dog implements Animal { barkVolume: Int! } +# Union Types for Testing + +# Search result union - tests union with existing types +union SearchResult = Product | User | Category + +# Action result union - tests success/error patterns +union ActionResult = ActionSuccess | ActionError + +type ActionSuccess { + message: String! + timestamp: String! +} + +type ActionError { + message: String! + code: String! +} + +# Input types for union operations +input SearchInput { + query: String! + limit: Int +} + +input ActionInput { + type: String! + payload: String! +} + type Query { _entities(representations: [_Any!]!): [_Entity!]! users: [User!]! @@ -158,6 +187,10 @@ type Query { filterCategories(filter: CategoryFilter!): [Category!]! randomPet: Animal! allPets: [Animal!]! + + # Union queries + search(input: SearchInput!): [SearchResult!]! + randomSearchResult: SearchResult! } input UserInput { @@ -166,6 +199,9 @@ input UserInput { type Mutation { createUser(input: UserInput!): User! + + # Union mutation + performAction(input: ActionInput!): ActionResult! } union _Entity = Product | Storage From 797c28d2871451cc9f5eebb819fdbaf945a58d0c Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 1 Jul 2025 14:02:01 +0200 Subject: [PATCH 03/13] chore: add more tests --- .../grpc_datasource/execution_plan_test.go | 1630 +++++++++++++---- .../grpc_datasource/grpc_datasource.go | 6 +- 2 files changed, 1298 insertions(+), 338 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index bb7dc353c1..76c83ceeeb 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -339,32 +339,9 @@ func TestQueryExecutionPlans(t *testing.T) { expectedError string }{ { - name: "Should include typename when requested", - query: `query UsersWithTypename { users { __typename id name } }`, - mapping: &GRPCMapping{ - QueryRPCs: map[string]RPCConfig{ - "users": { - RPC: "QueryUsers", - Request: "QueryUsersRequest", - Response: "QueryUsersResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "users": { - TargetName: "users", - }, - }, - "User": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - }, - }, + name: "Should include typename when requested", + query: `query UsersWithTypename { users { __typename id name } }`, + mapping: testMapping(), expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { @@ -415,20 +392,7 @@ func TestQueryExecutionPlans(t *testing.T) { users { id name } user(id: "1") { id name } }`, - mapping: &GRPCMapping{ - QueryRPCs: map[string]RPCConfig{ - "users": { - RPC: "QueryUsers", - Request: "QueryUsersRequest", - Response: "QueryUsersResponse", - }, - "user": { - RPC: "QueryUser", - Request: "QueryUserRequest", - Response: "QueryUserResponse", - }, - }, - }, + mapping: testMapping(), expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { @@ -508,36 +472,9 @@ func TestQueryExecutionPlans(t *testing.T) { }, }, { - name: "Should call query with two arguments and no variables and mapping for field names", - query: `query QueryWithTwoArguments { typeFilterWithArguments(filterField1: "test1", filterField2: "test2") { id name filterField1 filterField2 } }`, - mapping: &GRPCMapping{ - QueryRPCs: map[string]RPCConfig{ - "typeFilterWithArguments": { - RPC: "QueryTypeFilterWithArguments", - Request: "QueryTypeFilterWithArgumentsRequest", - Response: "QueryTypeFilterWithArgumentsResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "typeFilterWithArguments": { - TargetName: "type_filter_with_arguments", - ArgumentMappings: map[string]string{ - "filterField1": "filter_field_1", - "filterField2": "filter_field_2", - }, - }, - }, - "TypeWithMultipleFilterFields": { - "filterField1": { - TargetName: "filter_field_1", - }, - "filterField2": { - TargetName: "filter_field_2", - }, - }, - }, - }, + name: "Should call query with two arguments and no variables and mapping for field names", + query: `query QueryWithTwoArguments { typeFilterWithArguments(filterField1: "test1", filterField2: "test2") { id name filterField1 filterField2 } }`, + mapping: testMapping(), expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { @@ -599,37 +536,9 @@ func TestQueryExecutionPlans(t *testing.T) { }, }, { - name: "Should create an execution plan for a query with a complex input type and no variables and mapping for field names", - query: `query ComplexFilterTypeQuery { complexFilterType(filter: { name: "test", filterField1: "test1", filterField2: "test2", pagination: { page: 1, perPage: 10 } }) { id name } }`, - mapping: &GRPCMapping{ - QueryRPCs: RPCConfigMap{ - "complexFilterType": { - RPC: "QueryComplexFilterType", - Request: "QueryComplexFilterTypeRequest", - Response: "QueryComplexFilterTypeResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "complexFilterType": { - TargetName: "complex_filter_type", - }, - }, - "FilterType": { - "filterField1": { - TargetName: "filter_field1", - }, - "filterField2": { - TargetName: "filter_field2", - }, - }, - "Pagination": { - "perPage": { - TargetName: "per_page", - }, - }, - }, - }, + name: "Should create an execution plan for a query with a complex input type and no variables and mapping for field names", + query: `query ComplexFilterTypeQuery { complexFilterType(filter: { name: "test", filterField1: "test1", filterField2: "test2", pagination: { page: 1, perPage: 10 } }) { id name } }`, + mapping: testMapping(), expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { @@ -658,12 +567,12 @@ func TestQueryExecutionPlans(t *testing.T) { JSONPath: "name", }, { - Name: "filter_field1", + Name: "filter_field_1", TypeName: string(DataTypeString), JSONPath: "filterField1", }, { - Name: "filter_field2", + Name: "filter_field_2", TypeName: string(DataTypeString), JSONPath: "filterField2", }, @@ -1465,39 +1374,16 @@ func TestQueryExecutionPlans(t *testing.T) { } } -// TODO: Define test cases for interface execution plans -func TestInterfaceExecutionPlan(t *testing.T) { +func TestCompositeTypeExecutionPlan(t *testing.T) { tests := []struct { name string query string - mapping *GRPCMapping expectedPlan *RPCExecutionPlan expectedError string }{ { name: "Should create an execution plan for a query with a random cat", query: "query RandomCatQuery { randomPet { id name kind ... on Cat { meowVolume } } }", - mapping: &GRPCMapping{ - QueryRPCs: map[string]RPCConfig{ - "Query": { - RPC: "QueryRandomPet", - Request: "QueryRandomPetRequest", - Response: "QueryRandomPetResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "randomPet": { - TargetName: "random_pet", - }, - }, - "Cat": { - "meowVolume": { - TargetName: "meow_volume", - }, - }, - }, - }, expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { @@ -1550,113 +1436,31 @@ func TestInterfaceExecutionPlan(t *testing.T) { }, }, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - // Parse the GraphQL schema - schemaDoc := grpctest.MustGraphQLSchema(t) - - // Parse the GraphQL query - queryDoc, report := astparser.ParseGraphqlDocumentString(tt.query) - if report.HasErrors() { - t.Fatalf("failed to parse query: %s", report.Error()) - } - - walker := astvisitor.NewWalker(48) - - rpcPlanVisitor := newRPCPlanVisitor(&walker, rpcPlanVisitorConfig{ - subgraphName: "Products", - mapping: tt.mapping, - }) - - walker.Walk(&queryDoc, &schemaDoc, &report) - - if report.HasErrors() { - require.NotEmpty(t, tt.expectedError) - require.Contains(t, report.Error(), tt.expectedError) - return - } - - require.Empty(t, tt.expectedError) - diff := cmp.Diff(tt.expectedPlan, rpcPlanVisitor.plan) - if diff != "" { - t.Fatalf("execution plan mismatch: %s", diff) - } - }) - } -} - -// TODO: Define test cases for product execution plans -func TestProductExecutionPlan(t *testing.T) { - tests := []struct { - name string - query string - mapping *GRPCMapping - expectedPlan *RPCExecutionPlan - expectedError string - }{ { - name: "Should create an execution plan for a query with categories by kind", - query: "query CategoriesQuery($kind: CategoryKind!) { categoriesByKind(kind: $kind) { id name kind } }", - mapping: &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "categoriesByKind": { - RPC: "QueryCategoriesByKind", - Request: "QueryCategoriesByKindRequest", - Response: "QueryCategoriesByKindResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "categoriesByKind": { - TargetName: "categories_by_kind", - ArgumentMappings: map[string]string{ - "kind": "kind", - }, - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - }, - }, + name: "Should create an execution plan for a query with a random cat and dog", + query: "query CatAndDogQuery { randomPet { id name kind ... on Cat { meowVolume } ... on Dog { barkVolume } } }", expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { - ServiceName: "ProductService", - MethodName: "QueryCategoriesByKind", + ServiceName: "Products", + MethodName: "QueryRandomPet", Request: RPCMessage{ - Name: "QueryCategoriesByKindRequest", - Fields: []RPCField{ - { - Name: "kind", - TypeName: string(DataTypeEnum), - JSONPath: "kind", - EnumName: "CategoryKind", - }, - }, + Name: "QueryRandomPetRequest", }, Response: RPCMessage{ - Name: "QueryCategoriesByKindResponse", + Name: "QueryRandomPetResponse", Fields: []RPCField{ { - Name: "categories_by_kind", + Name: "random_pet", TypeName: string(DataTypeMessage), - JSONPath: "categoriesByKind", - Repeated: true, + JSONPath: "randomPet", Message: &RPCMessage{ - Name: "Category", + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ + "Cat", + "Dog", + }, Fields: []RPCField{ { Name: "id", @@ -1670,9 +1474,18 @@ func TestProductExecutionPlan(t *testing.T) { }, { Name: "kind", - TypeName: string(DataTypeEnum), + TypeName: string(DataTypeString), JSONPath: "kind", - EnumName: "CategoryKind", + }, + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", }, }, }, @@ -1684,66 +1497,30 @@ func TestProductExecutionPlan(t *testing.T) { }, }, { - name: "Should create an execution plan for a query with categories by kinds", - query: "query CategoriesQuery($kinds: [CategoryKind!]!) { categoriesByKinds(kinds: $kinds) { id name kind } }", - mapping: &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "categoriesByKinds": { - RPC: "QueryCategoriesByKinds", - Request: "QueryCategoriesByKindsRequest", - Response: "QueryCategoriesByKindsResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "categoriesByKinds": { - TargetName: "categories_by_kinds", - ArgumentMappings: map[string]string{ - "kinds": "kinds", - }, - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - }, - }, + name: "Should create an execution plan for a query with a union type", + query: "query UnionQuery { randomPet { id name kind ... on Cat { meowVolume } ... on Dog { barkVolume } } }", expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { - ServiceName: "ProductService", - MethodName: "QueryCategoriesByKinds", + ServiceName: "Products", + MethodName: "QueryRandomPet", Request: RPCMessage{ - Name: "QueryCategoriesByKindsRequest", - Fields: []RPCField{ - { - Name: "kinds", - TypeName: string(DataTypeEnum), - JSONPath: "kinds", - EnumName: "CategoryKind", - Repeated: true, - }, - }, + Name: "QueryRandomPetRequest", }, Response: RPCMessage{ - Name: "QueryCategoriesByKindsResponse", + Name: "QueryRandomPetResponse", Fields: []RPCField{ { - Name: "categories_by_kinds", + Name: "random_pet", TypeName: string(DataTypeMessage), - JSONPath: "categoriesByKinds", - Repeated: true, + JSONPath: "randomPet", Message: &RPCMessage{ - Name: "Category", + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ + "Cat", + "Dog", + }, Fields: []RPCField{ { Name: "id", @@ -1757,9 +1534,18 @@ func TestProductExecutionPlan(t *testing.T) { }, { Name: "kind", - TypeName: string(DataTypeEnum), + TypeName: string(DataTypeString), JSONPath: "kind", - EnumName: "CategoryKind", + }, + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", }, }, }, @@ -1771,71 +1557,749 @@ func TestProductExecutionPlan(t *testing.T) { }, }, { - name: "Should create an execution plan for a query with filtered categories", - query: "query FilterCategoriesQuery($filter: CategoryFilter!) { filterCategories(filter: $filter) { id name kind } }", - mapping: &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "filterCategories": { - RPC: "QueryFilterCategories", - Request: "QueryFilterCategoriesRequest", - Response: "QueryFilterCategoriesResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "filterCategories": { - TargetName: "filter_categories", - ArgumentMappings: map[string]string{ - "filter": "filter", - }, - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - "CategoryFilter": { - "category": { - TargetName: "category", - }, - "pagination": { - TargetName: "pagination", - }, - }, - "Pagination": { - "page": { - TargetName: "page", - }, - "perPage": { - TargetName: "per_page", - }, - }, - }, - }, + name: "Should create an execution plan for a query with all pets (interface list)", + query: "query AllPetsQuery { allPets { id name kind ... on Cat { meowVolume } ... on Dog { barkVolume } } }", expectedPlan: &RPCExecutionPlan{ Calls: []RPCCall{ { - ServiceName: "ProductService", - MethodName: "QueryFilterCategories", + ServiceName: "Products", + MethodName: "QueryAllPets", Request: RPCMessage{ - Name: "QueryFilterCategoriesRequest", + Name: "QueryAllPetsRequest", + }, + Response: RPCMessage{ + Name: "QueryAllPetsResponse", Fields: []RPCField{ { - Name: "filter", + Name: "all_pets", TypeName: string(DataTypeMessage), - JSONPath: "filter", + JSONPath: "allPets", + Repeated: true, Message: &RPCMessage{ - Name: "CategoryFilter", - Fields: []RPCField{ - { + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ + "Cat", + "Dog", + }, + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeString), + JSONPath: "kind", + }, + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a query with interface selecting only common fields", + query: "query CommonFieldsQuery { randomPet { id name kind } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryRandomPet", + Request: RPCMessage{ + Name: "QueryRandomPetRequest", + }, + Response: RPCMessage{ + Name: "QueryRandomPetResponse", + Fields: []RPCField{ + { + Name: "random_pet", + TypeName: string(DataTypeMessage), + JSONPath: "randomPet", + Message: &RPCMessage{ + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ + "Cat", + "Dog", + }, + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeString), + JSONPath: "kind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a SearchResult union query", + query: "query SearchQuery($input: SearchInput!) { search(input: $input) { ... on Product { id name price } ... on User { id name } ... on Category { id name kind } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QuerySearch", + Request: RPCMessage{ + Name: "QuerySearchRequest", + Fields: []RPCField{ + { + Name: "input", + TypeName: string(DataTypeMessage), + JSONPath: "input", + Message: &RPCMessage{ + Name: "SearchInput", + Fields: []RPCField{ + { + Name: "query", + TypeName: string(DataTypeString), + JSONPath: "query", + }, + { + Name: "limit", + TypeName: string(DataTypeInt32), + JSONPath: "limit", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "QuerySearchResponse", + Fields: []RPCField{ + { + Name: "search", + TypeName: string(DataTypeMessage), + JSONPath: "search", + Repeated: true, + Message: &RPCMessage{ + Name: "SearchResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "Product", + "User", + "Category", + }, + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "price", + TypeName: string(DataTypeDouble), + JSONPath: "price", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a single SearchResult union query", + query: "query RandomSearchQuery { randomSearchResult { ... on Product { id name price } ... on User { id name } ... on Category { id name kind } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryRandomSearchResult", + Request: RPCMessage{ + Name: "QueryRandomSearchResultRequest", + }, + Response: RPCMessage{ + Name: "QueryRandomSearchResultResponse", + Fields: []RPCField{ + { + Name: "random_search_result", + TypeName: string(DataTypeMessage), + JSONPath: "randomSearchResult", + Message: &RPCMessage{ + Name: "SearchResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "Product", + "User", + "Category", + }, + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "price", + TypeName: string(DataTypeDouble), + JSONPath: "price", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a SearchResult union with partial selection", + query: "query PartialSearchQuery($input: SearchInput!) { search(input: $input) { ... on Product { id name } ... on User { id name } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QuerySearch", + Request: RPCMessage{ + Name: "QuerySearchRequest", + Fields: []RPCField{ + { + Name: "input", + TypeName: string(DataTypeMessage), + JSONPath: "input", + Message: &RPCMessage{ + Name: "SearchInput", + Fields: []RPCField{ + { + Name: "query", + TypeName: string(DataTypeString), + JSONPath: "query", + }, + { + Name: "limit", + TypeName: string(DataTypeInt32), + JSONPath: "limit", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "QuerySearchResponse", + Fields: []RPCField{ + { + Name: "search", + TypeName: string(DataTypeMessage), + JSONPath: "search", + Repeated: true, + Message: &RPCMessage{ + Name: "SearchResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "Product", + "User", + "Category", + }, + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse the GraphQL schema + schemaDoc := grpctest.MustGraphQLSchema(t) + + // Parse the GraphQL query + queryDoc, report := astparser.ParseGraphqlDocumentString(tt.query) + if report.HasErrors() { + t.Fatalf("failed to parse query: %s", report.Error()) + } + + walker := astvisitor.NewWalker(48) + + rpcPlanVisitor := newRPCPlanVisitor(&walker, rpcPlanVisitorConfig{ + subgraphName: "Products", + mapping: testMapping(), + }) + + walker.Walk(&queryDoc, &schemaDoc, &report) + + if report.HasErrors() { + require.NotEmpty(t, tt.expectedError) + require.Contains(t, report.Error(), tt.expectedError) + return + } + + require.Empty(t, tt.expectedError) + diff := cmp.Diff(tt.expectedPlan, rpcPlanVisitor.plan) + if diff != "" { + t.Fatalf("execution plan mismatch: %s", diff) + } + }) + } +} + +func TestMutationUnionExecutionPlan(t *testing.T) { + tests := []struct { + name string + query string + expectedPlan *RPCExecutionPlan + expectedError string + }{ + { + name: "Should create an execution plan for ActionResult union mutation", + query: "mutation PerformActionMutation($input: ActionInput!) { performAction(input: $input) { ... on ActionSuccess { message timestamp } ... on ActionError { message code } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "MutationPerformAction", + Request: RPCMessage{ + Name: "MutationPerformActionRequest", + Fields: []RPCField{ + { + Name: "input", + TypeName: string(DataTypeMessage), + JSONPath: "input", + Message: &RPCMessage{ + Name: "ActionInput", + Fields: []RPCField{ + { + Name: "type", + TypeName: string(DataTypeString), + JSONPath: "type", + }, + { + Name: "payload", + TypeName: string(DataTypeString), + JSONPath: "payload", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "MutationPerformActionResponse", + Fields: []RPCField{ + { + Name: "perform_action", + TypeName: string(DataTypeMessage), + JSONPath: "performAction", + Message: &RPCMessage{ + Name: "ActionResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "ActionSuccess", + "ActionError", + }, + Fields: []RPCField{ + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "timestamp", + TypeName: string(DataTypeString), + JSONPath: "timestamp", + }, + { + Name: "code", + TypeName: string(DataTypeString), + JSONPath: "code", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for ActionResult union with only success case", + query: "mutation PerformSuccessActionMutation($input: ActionInput!) { performAction(input: $input) { ... on ActionSuccess { message timestamp } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "MutationPerformAction", + Request: RPCMessage{ + Name: "MutationPerformActionRequest", + Fields: []RPCField{ + { + Name: "input", + TypeName: string(DataTypeMessage), + JSONPath: "input", + Message: &RPCMessage{ + Name: "ActionInput", + Fields: []RPCField{ + { + Name: "type", + TypeName: string(DataTypeString), + JSONPath: "type", + }, + { + Name: "payload", + TypeName: string(DataTypeString), + JSONPath: "payload", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "MutationPerformActionResponse", + Fields: []RPCField{ + { + Name: "perform_action", + TypeName: string(DataTypeMessage), + JSONPath: "performAction", + Message: &RPCMessage{ + Name: "ActionResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "ActionSuccess", + "ActionError", + }, + Fields: []RPCField{ + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "timestamp", + TypeName: string(DataTypeString), + JSONPath: "timestamp", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for ActionResult union with only error case", + query: "mutation PerformErrorActionMutation($input: ActionInput!) { performAction(input: $input) { ... on ActionError { message code } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "MutationPerformAction", + Request: RPCMessage{ + Name: "MutationPerformActionRequest", + Fields: []RPCField{ + { + Name: "input", + TypeName: string(DataTypeMessage), + JSONPath: "input", + Message: &RPCMessage{ + Name: "ActionInput", + Fields: []RPCField{ + { + Name: "type", + TypeName: string(DataTypeString), + JSONPath: "type", + }, + { + Name: "payload", + TypeName: string(DataTypeString), + JSONPath: "payload", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "MutationPerformActionResponse", + Fields: []RPCField{ + { + Name: "perform_action", + TypeName: string(DataTypeMessage), + JSONPath: "performAction", + Message: &RPCMessage{ + Name: "ActionResult", + OneOfType: OneOfTypeUnion, + MemberTypes: []string{ + "ActionSuccess", + "ActionError", + }, + Fields: []RPCField{ + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "code", + TypeName: string(DataTypeString), + JSONPath: "code", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse the GraphQL schema + schemaDoc := grpctest.MustGraphQLSchema(t) + + // Parse the GraphQL query + queryDoc, report := astparser.ParseGraphqlDocumentString(tt.query) + if report.HasErrors() { + t.Fatalf("failed to parse query: %s", report.Error()) + } + + walker := astvisitor.NewWalker(48) + + rpcPlanVisitor := newRPCPlanVisitor(&walker, rpcPlanVisitorConfig{ + subgraphName: "Products", + mapping: testMapping(), + }) + + walker.Walk(&queryDoc, &schemaDoc, &report) + + if report.HasErrors() { + require.NotEmpty(t, tt.expectedError) + require.Contains(t, report.Error(), tt.expectedError) + return + } + + require.Empty(t, tt.expectedError) + diff := cmp.Diff(tt.expectedPlan, rpcPlanVisitor.plan) + if diff != "" { + t.Fatalf("execution plan mismatch: %s", diff) + } + }) + } +} + +// TODO: Define test cases for product execution plans +func TestProductExecutionPlan(t *testing.T) { + tests := []struct { + name string + query string + expectedPlan *RPCExecutionPlan + expectedError string + }{ + { + name: "Should create an execution plan for a query with categories by kind", + query: "query CategoriesQuery($kind: CategoryKind!) { categoriesByKind(kind: $kind) { id name kind } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryCategoriesByKind", + Request: RPCMessage{ + Name: "QueryCategoriesByKindRequest", + Fields: []RPCField{ + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, + }, + }, + Response: RPCMessage{ + Name: "QueryCategoriesByKindResponse", + Fields: []RPCField{ + { + Name: "categories_by_kind", + TypeName: string(DataTypeMessage), + JSONPath: "categoriesByKind", + Repeated: true, + Message: &RPCMessage{ + Name: "Category", + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a query with categories by kinds", + query: "query CategoriesQuery($kinds: [CategoryKind!]!) { categoriesByKinds(kinds: $kinds) { id name kind } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryCategoriesByKinds", + Request: RPCMessage{ + Name: "QueryCategoriesByKindsRequest", + Fields: []RPCField{ + { + Name: "kinds", + TypeName: string(DataTypeEnum), + JSONPath: "kinds", + EnumName: "CategoryKind", + Repeated: true, + }, + }, + }, + Response: RPCMessage{ + Name: "QueryCategoriesByKindsResponse", + Fields: []RPCField{ + { + Name: "categories_by_kinds", + TypeName: string(DataTypeMessage), + JSONPath: "categoriesByKinds", + Repeated: true, + Message: &RPCMessage{ + Name: "Category", + Fields: []RPCField{ + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create an execution plan for a query with filtered categories", + query: "query FilterCategoriesQuery($filter: CategoryFilter!) { filterCategories(filter: $filter) { id name kind } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryFilterCategories", + Request: RPCMessage{ + Name: "QueryFilterCategoriesRequest", + Fields: []RPCField{ + { + Name: "filter", + TypeName: string(DataTypeMessage), + JSONPath: "filter", + Message: &RPCMessage{ + Name: "CategoryFilter", + Fields: []RPCField{ + { Name: "category", TypeName: string(DataTypeEnum), JSONPath: "category", @@ -1927,7 +2391,7 @@ func TestProductExecutionPlan(t *testing.T) { t.Fatalf("failed to validate query: %s", report.Error()) } - planner := NewPlanner("Products", tt.mapping) + planner := NewPlanner("Products", testMapping()) outPlan, err := planner.PlanOperation(&queryDoc, &schemaDoc) if tt.expectedError != "" { @@ -1951,3 +2415,499 @@ func TestProductExecutionPlan(t *testing.T) { }) } } + +func testMapping() *GRPCMapping { + return &GRPCMapping{ + Service: "Products", + QueryRPCs: map[string]RPCConfig{ + "users": { + RPC: "QueryUsers", + Request: "QueryUsersRequest", + Response: "QueryUsersResponse", + }, + "user": { + RPC: "QueryUser", + Request: "QueryUserRequest", + Response: "QueryUserResponse", + }, + "nestedType": { + RPC: "QueryNestedType", + Request: "QueryNestedTypeRequest", + Response: "QueryNestedTypeResponse", + }, + "recursiveType": { + RPC: "QueryRecursiveType", + Request: "QueryRecursiveTypeRequest", + Response: "QueryRecursiveTypeResponse", + }, + "typeFilterWithArguments": { + RPC: "QueryTypeFilterWithArguments", + Request: "QueryTypeFilterWithArgumentsRequest", + Response: "QueryTypeFilterWithArgumentsResponse", + }, + "typeWithMultipleFilterFields": { + RPC: "QueryTypeWithMultipleFilterFields", + Request: "QueryTypeWithMultipleFilterFieldsRequest", + Response: "QueryTypeWithMultipleFilterFieldsResponse", + }, + "complexFilterType": { + RPC: "QueryComplexFilterType", + Request: "QueryComplexFilterTypeRequest", + Response: "QueryComplexFilterTypeResponse", + }, + "calculateTotals": { + RPC: "QueryCalculateTotals", + Request: "QueryCalculateTotalsRequest", + Response: "QueryCalculateTotalsResponse", + }, + "randomPet": { + RPC: "QueryRandomPet", + Request: "QueryRandomPetRequest", + Response: "QueryRandomPetResponse", + }, + "allPets": { + RPC: "QueryAllPets", + Request: "QueryAllPetsRequest", + Response: "QueryAllPetsResponse", + }, + "categories": { + RPC: "QueryCategories", + Request: "QueryCategoriesRequest", + Response: "QueryCategoriesResponse", + }, + "categoriesByKind": { + RPC: "QueryCategoriesByKind", + Request: "QueryCategoriesByKindRequest", + Response: "QueryCategoriesByKindResponse", + }, + "categoriesByKinds": { + RPC: "QueryCategoriesByKinds", + Request: "QueryCategoriesByKindsRequest", + Response: "QueryCategoriesByKindsResponse", + }, + "filterCategories": { + RPC: "QueryFilterCategories", + Request: "QueryFilterCategoriesRequest", + Response: "QueryFilterCategoriesResponse", + }, + "randomSearchResult": { + RPC: "QueryRandomSearchResult", + Request: "QueryRandomSearchResultRequest", + Response: "QueryRandomSearchResultResponse", + }, + "search": { + RPC: "QuerySearch", + Request: "QuerySearchRequest", + Response: "QuerySearchResponse", + }, + }, + MutationRPCs: RPCConfigMap{ + "createUser": { + RPC: "CreateUser", + Request: "CreateUserRequest", + Response: "CreateUserResponse", + }, + "performAction": { + RPC: "MutationPerformAction", + Request: "MutationPerformActionRequest", + Response: "MutationPerformActionResponse", + }, + }, + SubscriptionRPCs: RPCConfigMap{}, + EntityRPCs: map[string]EntityRPCConfig{ + "Product": { + Key: "id", + RPCConfig: RPCConfig{ + RPC: "LookupProductById", + Request: "LookupProductByIdRequest", + Response: "LookupProductByIdResponse", + }, + }, + "Storage": { + Key: "id", + RPCConfig: RPCConfig{ + RPC: "LookupStorageById", + Request: "LookupStorageByIdRequest", + Response: "LookupStorageByIdResponse", + }, + }, + }, + EnumValues: map[string][]EnumValueMapping{ + "CategoryKind": { + {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, + {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, + {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, + {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, + }, + }, + Fields: map[string]FieldMap{ + "Query": { + "user": { + TargetName: "user", + ArgumentMappings: map[string]string{ + "id": "id", + }, + }, + "nestedType": { + TargetName: "nested_type", + }, + "recursiveType": { + TargetName: "recursive_type", + }, + "randomPet": { + TargetName: "random_pet", + }, + "allPets": { + TargetName: "all_pets", + }, + "categories": { + TargetName: "categories", + }, + "categoriesByKind": { + TargetName: "categories_by_kind", + ArgumentMappings: map[string]string{ + "kind": "kind", + }, + }, + "categoriesByKinds": { + TargetName: "categories_by_kinds", + ArgumentMappings: map[string]string{ + "kinds": "kinds", + }, + }, + "filterCategories": { + TargetName: "filter_categories", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "typeFilterWithArguments": { + TargetName: "type_filter_with_arguments", + ArgumentMappings: map[string]string{ + "filterField1": "filter_field_1", + "filterField2": "filter_field_2", + }, + }, + "typeWithMultipleFilterFields": { + TargetName: "type_with_multiple_filter_fields", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "complexFilterType": { + TargetName: "complex_filter_type", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "calculateTotals": { + TargetName: "calculate_totals", + ArgumentMappings: map[string]string{ + "orders": "orders", + }, + }, + "search": { + TargetName: "search", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + "randomSearchResult": { + TargetName: "random_search_result", + }, + }, + "Mutation": { + "createUser": { + TargetName: "create_user", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + "performAction": { + TargetName: "perform_action", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + }, + "UserInput": { + "name": { + TargetName: "name", + }, + }, + "Product": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "price": { + TargetName: "price", + }, + }, + "Storage": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "location": { + TargetName: "location", + }, + }, + "User": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "NestedTypeA": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "b": { + TargetName: "b", + }, + }, + "NestedTypeB": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "c": { + TargetName: "c", + }, + }, + "NestedTypeC": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "RecursiveType": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "recursiveType": { + TargetName: "recursive_type", + }, + }, + "TypeWithMultipleFilterFields": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "filterField1": { + TargetName: "filter_field_1", + }, + "filterField2": { + TargetName: "filter_field_2", + }, + }, + "TypeWithComplexFilterInput": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "Cat": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "meowVolume": { + TargetName: "meow_volume", + }, + }, + "Dog": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "barkVolume": { + TargetName: "bark_volume", + }, + }, + "Animal": { + "cat": { + TargetName: "cat", + }, + "dog": { + TargetName: "dog", + }, + }, + "FilterType": { + "name": { + TargetName: "name", + }, + "filterField1": { + TargetName: "filter_field_1", + }, + "filterField2": { + TargetName: "filter_field_2", + }, + "pagination": { + TargetName: "pagination", + }, + }, + "Pagination": { + "page": { + TargetName: "page", + }, + "perPage": { + TargetName: "per_page", + }, + }, + "ComplexFilterTypeInput": { + "filter": { + TargetName: "filter", + }, + }, + "Category": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + }, + "CategoryFilter": { + "category": { + TargetName: "category", + }, + "pagination": { + TargetName: "pagination", + }, + }, + "Order": { + "orderId": { + TargetName: "order_id", + }, + "customerName": { + TargetName: "customer_name", + }, + "totalItems": { + TargetName: "total_items", + }, + "orderLines": { + TargetName: "order_lines", + }, + }, + "OrderLine": { + "productId": { + TargetName: "product_id", + }, + "quantity": { + TargetName: "quantity", + }, + "modifiers": { + TargetName: "modifiers", + }, + }, + "OrderInput": { + "orderId": { + TargetName: "order_id", + }, + "customerName": { + TargetName: "customer_name", + }, + "lines": { + TargetName: "lines", + }, + }, + "OrderLineInput": { + "productId": { + TargetName: "product_id", + }, + "quantity": { + TargetName: "quantity", + }, + "modifiers": { + TargetName: "modifiers", + }, + }, + "ActionSuccess": { + "message": { + TargetName: "message", + }, + "timestamp": { + TargetName: "timestamp", + }, + }, + "ActionError": { + "message": { + TargetName: "message", + }, + "code": { + TargetName: "code", + }, + }, + "SearchInput": { + "query": { + TargetName: "query", + }, + "limit": { + TargetName: "limit", + }, + }, + "ActionInput": { + "type": { + TargetName: "type", + }, + "payload": { + TargetName: "payload", + }, + }, + "SearchResult": { + "product": { + TargetName: "product", + }, + "user": { + TargetName: "user", + }, + "category": { + TargetName: "category", + }, + }, + "ActionResult": { + "actionSuccess": { + TargetName: "action_success", + }, + "actionError": { + TargetName: "action_error", + }, + }, + }, + } +} diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index f7e86bfbbc..cf45c4054d 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -162,9 +162,9 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa continue } - for _, implementedBy := range message.MemberTypes { - if implementedBy == string(data.Type().Descriptor().Name()) { - root.Set(field.JSONPath, arena.NewString(implementedBy)) + for _, memberTypes := range message.MemberTypes { + if memberTypes == string(data.Type().Descriptor().Name()) { + root.Set(field.JSONPath, arena.NewString(memberTypes)) break } } From b2cf9e3aaa585662ad75a3c9931b6df5e6a6244d Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 1 Jul 2025 16:41:49 +0200 Subject: [PATCH 04/13] feat: use field selections to request only valid data --- .../grpc_datasource/execution_plan.go | 24 ++ .../grpc_datasource/execution_plan_test.go | 339 ++++++++++++------ .../grpc_datasource/execution_plan_visitor.go | 32 +- .../grpc_datasource/grpc_datasource.go | 4 +- .../grpc_datasource/grpc_datasource_test.go | 220 ++++++++++++ 5 files changed, 497 insertions(+), 122 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index 0faebc2658..60108d0e4c 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -74,6 +74,8 @@ type RPCMessage struct { Name string // Fields is a list of fields in the message Fields RPCFields + // FieldSelectionSet are field selections based on inline fragments + FieldSelectionSet RPCFieldSelectionSet // OneOfType indicates the type of the oneof field OneOfType OneOfType // MemberTypes provides the names of the types that are implemented by the Interface or Union @@ -85,6 +87,28 @@ func (r *RPCMessage) IsOneOf() bool { return r.OneOfType > OneOfTypeNone && r.OneOfType <= OneOfTypeUnion } +// RPCFieldSelectionSet is a map of field selections based on inline fragments +type RPCFieldSelectionSet map[string]RPCFields + +// Add adds a field selection set to the map +func (r RPCFieldSelectionSet) Add(fragmentName string, field RPCField) { + if r[fragmentName] == nil { + r[fragmentName] = make(RPCFields, 0) + } + + r[fragmentName] = append(r[fragmentName], field) +} + +// FieldsForSelection returns the fields for a given fragment name. +func (r RPCFieldSelectionSet) FieldsForSelection(fragmentName string) RPCFields { + fields, ok := r[fragmentName] + if !ok { + return nil + } + + return fields +} + // RPCField represents a single field in a gRPC message. // It contains all information required to extract data from GraphQL variables // and construct the appropriate protobuf field. diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index 76c83ceeeb..3319d9a0d1 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -1406,6 +1406,15 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "Cat", "Dog", }, + FieldSelectionSet: RPCFieldSelectionSet{ + "Cat": { + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + }, + }, Fields: []RPCField{ { Name: "id", @@ -1422,11 +1431,6 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { TypeName: string(DataTypeString), JSONPath: "kind", }, - { - Name: "meow_volume", - TypeName: string(DataTypeInt32), - JSONPath: "meowVolume", - }, }, }, }, @@ -1461,6 +1465,22 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "Cat", "Dog", }, + FieldSelectionSet: RPCFieldSelectionSet{ + "Cat": { + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + }, + "Dog": { + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", + }, + }, + }, Fields: []RPCField{ { Name: "id", @@ -1477,16 +1497,6 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { TypeName: string(DataTypeString), JSONPath: "kind", }, - { - Name: "meow_volume", - TypeName: string(DataTypeInt32), - JSONPath: "meowVolume", - }, - { - Name: "bark_volume", - TypeName: string(DataTypeInt32), - JSONPath: "barkVolume", - }, }, }, }, @@ -1521,6 +1531,22 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "Cat", "Dog", }, + FieldSelectionSet: RPCFieldSelectionSet{ + "Cat": { + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + }, + "Dog": { + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", + }, + }, + }, Fields: []RPCField{ { Name: "id", @@ -1537,16 +1563,6 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { TypeName: string(DataTypeString), JSONPath: "kind", }, - { - Name: "meow_volume", - TypeName: string(DataTypeInt32), - JSONPath: "meowVolume", - }, - { - Name: "bark_volume", - TypeName: string(DataTypeInt32), - JSONPath: "barkVolume", - }, }, }, }, @@ -1582,6 +1598,22 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "Cat", "Dog", }, + FieldSelectionSet: RPCFieldSelectionSet{ + "Cat": { + { + Name: "meow_volume", + TypeName: string(DataTypeInt32), + JSONPath: "meowVolume", + }, + }, + "Dog": { + { + Name: "bark_volume", + TypeName: string(DataTypeInt32), + JSONPath: "barkVolume", + }, + }, + }, Fields: []RPCField{ { Name: "id", @@ -1598,16 +1630,6 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { TypeName: string(DataTypeString), JSONPath: "kind", }, - { - Name: "meow_volume", - TypeName: string(DataTypeInt32), - JSONPath: "meowVolume", - }, - { - Name: "bark_volume", - TypeName: string(DataTypeInt32), - JSONPath: "barkVolume", - }, }, }, }, @@ -1716,27 +1738,54 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "User", "Category", }, - Fields: []RPCField{ - { - Name: "id", - TypeName: string(DataTypeString), - JSONPath: "id", - }, - { - Name: "name", - TypeName: string(DataTypeString), - JSONPath: "name", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "Product": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "price", + TypeName: string(DataTypeDouble), + JSONPath: "price", + }, }, - { - Name: "price", - TypeName: string(DataTypeDouble), - JSONPath: "price", + "User": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, }, - { - Name: "kind", - TypeName: string(DataTypeEnum), - JSONPath: "kind", - EnumName: "CategoryKind", + "Category": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, }, }, }, @@ -1773,27 +1822,54 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "User", "Category", }, - Fields: []RPCField{ - { - Name: "id", - TypeName: string(DataTypeString), - JSONPath: "id", - }, - { - Name: "name", - TypeName: string(DataTypeString), - JSONPath: "name", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "Product": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "price", + TypeName: string(DataTypeDouble), + JSONPath: "price", + }, }, - { - Name: "price", - TypeName: string(DataTypeDouble), - JSONPath: "price", + "User": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, }, - { - Name: "kind", - TypeName: string(DataTypeEnum), - JSONPath: "kind", - EnumName: "CategoryKind", + "Category": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeEnum), + JSONPath: "kind", + EnumName: "CategoryKind", + }, }, }, }, @@ -1853,16 +1929,31 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { "User", "Category", }, - Fields: []RPCField{ - { - Name: "id", - TypeName: string(DataTypeString), - JSONPath: "id", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "Product": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, }, - { - Name: "name", - TypeName: string(DataTypeString), - JSONPath: "name", + "User": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, }, }, }, @@ -1965,21 +2056,31 @@ func TestMutationUnionExecutionPlan(t *testing.T) { "ActionSuccess", "ActionError", }, - Fields: []RPCField{ - { - Name: "message", - TypeName: string(DataTypeString), - JSONPath: "message", - }, - { - Name: "timestamp", - TypeName: string(DataTypeString), - JSONPath: "timestamp", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "ActionSuccess": { + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "timestamp", + TypeName: string(DataTypeString), + JSONPath: "timestamp", + }, }, - { - Name: "code", - TypeName: string(DataTypeString), - JSONPath: "code", + "ActionError": { + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "code", + TypeName: string(DataTypeString), + JSONPath: "code", + }, }, }, }, @@ -2037,16 +2138,19 @@ func TestMutationUnionExecutionPlan(t *testing.T) { "ActionSuccess", "ActionError", }, - Fields: []RPCField{ - { - Name: "message", - TypeName: string(DataTypeString), - JSONPath: "message", - }, - { - Name: "timestamp", - TypeName: string(DataTypeString), - JSONPath: "timestamp", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "ActionSuccess": { + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "timestamp", + TypeName: string(DataTypeString), + JSONPath: "timestamp", + }, }, }, }, @@ -2104,16 +2208,19 @@ func TestMutationUnionExecutionPlan(t *testing.T) { "ActionSuccess", "ActionError", }, - Fields: []RPCField{ - { - Name: "message", - TypeName: string(DataTypeString), - JSONPath: "message", - }, - { - Name: "code", - TypeName: string(DataTypeString), - JSONPath: "code", + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "ActionError": { + { + Name: "message", + TypeName: string(DataTypeString), + JSONPath: "message", + }, + { + Name: "code", + TypeName: string(DataTypeString), + JSONPath: "code", + }, }, }, }, diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go index de7ad9e6a7..f1ad122aef 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor.go @@ -188,8 +188,7 @@ func (r *rpcPlanVisitor) handleCompositeType(node ast.Node) error { switch node.Kind { case ast.NodeKindField: - r.handleCompositeType(r.walker.EnclosingTypeDefinition) - return nil + return r.handleCompositeType(r.walker.EnclosingTypeDefinition) case ast.NodeKindInterfaceTypeDefinition: oneOfType = OneOfTypeInterface memberTypes, ok = r.definition.InterfaceTypeDefinitionImplementedByObjectWithNames(node.Ref) @@ -250,10 +249,23 @@ func (r *rpcPlanVisitor) LeaveInlineFragment(ref int) { } } -func (r *rpcPlanVisitor) isInRootField() bool { +func (r *rpcPlanVisitor) IsRootField() bool { return len(r.walker.Ancestors) == 2 && r.walker.Ancestors[0].Kind == ast.NodeKindOperationDefinition } +func (r *rpcPlanVisitor) IsInlineFragmentField() (int, bool) { + if len(r.walker.Ancestors) < 2 { + return -1, false + } + + node := r.walker.Ancestors[len(r.walker.Ancestors)-2] + if node.Kind != ast.NodeKindInlineFragment { + return -1, false + } + + return node.Ref, true +} + func (r *rpcPlanVisitor) handleRootField(ref int) error { r.operationFieldRef = ref r.planInfo.operationFieldName = r.operation.FieldNameString(ref) @@ -277,7 +289,7 @@ func (r *rpcPlanVisitor) handleRootField(ref int) error { // EnterField implements astvisitor.EnterFieldVisitor. func (r *rpcPlanVisitor) EnterField(ref int) { fieldName := r.operation.FieldNameString(ref) - if r.isInRootField() { + if r.IsRootField() { if err := r.handleRootField(ref); err != nil { r.walker.StopWithInternalErr(err) return @@ -331,6 +343,16 @@ func (r *rpcPlanVisitor) EnterField(ref int) { field.StaticValue = parentTypeName } + if ref, ok := r.IsInlineFragmentField(); ok && !r.planInfo.isEntityLookup { + if r.planInfo.currentResponseMessage.FieldSelectionSet == nil { + r.planInfo.currentResponseMessage.FieldSelectionSet = make(RPCFieldSelectionSet) + } + + inlineFragmentName := r.operation.InlineFragmentTypeConditionNameString(ref) + r.planInfo.currentResponseMessage.FieldSelectionSet.Add(inlineFragmentName, field) + return + } + r.planInfo.currentResponseMessage.Fields = append(r.planInfo.currentResponseMessage.Fields, field) } @@ -341,7 +363,7 @@ func (r *rpcPlanVisitor) LeaveField(ref int) { } // If we are not in the operation field, we can increment the response field index. - if !r.isInRootField() { + if !r.IsRootField() { r.planInfo.currentResponseFieldIndex++ return } diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index cf45c4054d..f353979e3b 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -155,7 +155,9 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa } } - for _, field := range message.Fields { + validFields := append(message.Fields, message.FieldSelectionSet.FieldsForSelection(string(data.Type().Descriptor().Name()))...) + + for _, field := range validFields { if field.StaticValue != "" { if len(message.MemberTypes) == 0 { root.Set(field.JSONPath, arena.NewString(field.StaticValue)) diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index 156976b6b8..f7297fb541 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -722,16 +722,198 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { require.Contains(t, searchResult, "id") require.Contains(t, searchResult, "name") require.Contains(t, searchResult, "price") + require.Equal(t, "product-random-1", searchResult["id"]) + require.Equal(t, "Random Product", searchResult["name"]) + require.Equal(t, 29.99, searchResult["price"]) case "User": require.Contains(t, searchResult, "id") require.Contains(t, searchResult, "name") + require.Equal(t, "user-random-1", searchResult["id"]) + require.Equal(t, "Random User", searchResult["name"]) case "Category": require.Contains(t, searchResult, "id") require.Contains(t, searchResult, "name") require.Contains(t, searchResult, "kind") + require.Equal(t, "category-random-1", searchResult["id"]) + require.Equal(t, "Random Category", searchResult["name"]) + require.Equal(t, "ELECTRONICS", searchResult["kind"]) + default: + t.Fatalf("Unexpected __typename: %s", typeName) } }, }, + { + name: "Query search with input - mixed results", + query: `query($input: SearchInput!) { search(input: $input) { __typename ... on Product { id name price } ... on User { id name } ... on Category { id name kind } } }`, + vars: `{"variables":{"input":{"query":"test","limit":6}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + searchResults, ok := data["search"].([]interface{}) + require.True(t, ok, "search should be an array") + require.NotEmpty(t, searchResults, "search should not be empty") + require.Len(t, searchResults, 6, "should return 6 results as per limit") + + // Verify we have a mix of all three types (Product, User, Category) + var productCount, userCount, categoryCount int + for i, result := range searchResults { + searchResult := result.(map[string]interface{}) + require.Contains(t, searchResult, "__typename") + typeName := searchResult["__typename"].(string) + + switch typeName { + case "Product": + productCount++ + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + require.Contains(t, searchResult, "price") + expectedID := fmt.Sprintf("product-search-%d", (i/3)*3+1) + require.Equal(t, expectedID, searchResult["id"]) + require.Contains(t, searchResult["name"].(string), "Product matching 'test'") + case "User": + userCount++ + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + expectedID := fmt.Sprintf("user-search-%d", ((i-1)/3)*3+2) + require.Equal(t, expectedID, searchResult["id"]) + require.Contains(t, searchResult["name"].(string), "User matching 'test'") + case "Category": + categoryCount++ + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + require.Contains(t, searchResult, "kind") + expectedID := fmt.Sprintf("category-search-%d", ((i-2)/3)*3+3) + require.Equal(t, expectedID, searchResult["id"]) + require.Contains(t, searchResult["name"].(string), "Category matching 'test'") + default: + t.Fatalf("Unexpected __typename: %s", typeName) + } + } + + // Verify we have exactly 2 of each type (cycling through Product, User, Category) + require.Equal(t, 2, productCount, "should have 2 products") + require.Equal(t, 2, userCount, "should have 2 users") + require.Equal(t, 2, categoryCount, "should have 2 categories") + }, + }, + { + name: "Query search with limited results", + query: `query($input: SearchInput!) { search(input: $input) { __typename ... on Product { id name } ... on User { id name } } }`, + vars: `{"variables":{"input":{"query":"limited","limit":3}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + searchResults, ok := data["search"].([]interface{}) + require.True(t, ok, "search should be an array") + require.NotEmpty(t, searchResults, "search should not be empty") + require.Len(t, searchResults, 3, "should return 3 results as per limit") + + // Only check Product and User types since Category fragments are not selected + for _, result := range searchResults { + searchResult := result.(map[string]interface{}) + require.Contains(t, searchResult, "__typename") + typeName := searchResult["__typename"].(string) + + switch typeName { + case "Product": + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + require.NotContains(t, searchResult, "price", "price should not be selected") + case "User": + require.Contains(t, searchResult, "id") + require.Contains(t, searchResult, "name") + case "Category": + // Category should still have __typename, but won't have other fields since they weren't selected + require.Contains(t, searchResult, "__typename") + require.NotContains(t, searchResult, "name", "name should not be selected for Category") + require.NotContains(t, searchResult, "kind", "kind should not be selected for Category") + default: + t.Fatalf("Unexpected __typename: %s", typeName) + } + } + }, + }, + { + name: "Mutation perform action - success case", + query: `mutation($input: ActionInput!) { performAction(input: $input) { __typename ... on ActionSuccess { message timestamp } ... on ActionError { message code } } }`, + vars: `{"variables":{"input":{"type":"create_user","payload":"user data"}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + actionResult, ok := data["performAction"].(map[string]interface{}) + require.True(t, ok, "performAction should be an object") + require.NotEmpty(t, actionResult, "performAction should not be empty") + require.Contains(t, actionResult, "__typename") + require.Equal(t, "ActionSuccess", actionResult["__typename"]) + + require.Contains(t, actionResult, "message") + require.Contains(t, actionResult, "timestamp") + require.Equal(t, "Action 'create_user' completed successfully", actionResult["message"]) + require.Equal(t, "2024-01-01T00:00:00Z", actionResult["timestamp"]) + }, + }, + { + name: "Mutation perform action - validation error case", + query: `mutation($input: ActionInput!) { performAction(input: $input) { __typename ... on ActionSuccess { message timestamp } ... on ActionError { message code } } }`, + vars: `{"variables":{"input":{"type":"error_action","payload":"invalid data"}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + actionResult, ok := data["performAction"].(map[string]interface{}) + require.True(t, ok, "performAction should be an object") + require.NotEmpty(t, actionResult, "performAction should not be empty") + require.Contains(t, actionResult, "__typename") + require.Equal(t, "ActionError", actionResult["__typename"]) + + require.Contains(t, actionResult, "message") + require.Contains(t, actionResult, "code") + require.Equal(t, "Action failed due to validation error", actionResult["message"]) + require.Equal(t, "VALIDATION_ERROR", actionResult["code"]) + }, + }, + { + name: "Mutation perform action - invalid action error case", + query: `mutation($input: ActionInput!) { performAction(input: $input) { __typename ... on ActionSuccess { message timestamp } ... on ActionError { message code } } }`, + vars: `{"variables":{"input":{"type":"invalid_action","payload":"test"}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + actionResult, ok := data["performAction"].(map[string]interface{}) + require.True(t, ok, "performAction should be an object") + require.NotEmpty(t, actionResult, "performAction should not be empty") + require.Contains(t, actionResult, "__typename") + require.Equal(t, "ActionError", actionResult["__typename"]) + + require.Contains(t, actionResult, "message") + require.Contains(t, actionResult, "code") + require.Equal(t, "Invalid action type provided", actionResult["message"]) + require.Equal(t, "INVALID_ACTION", actionResult["code"]) + }, + }, + { + name: "Mutation perform action - only success fragment", + query: `mutation($input: ActionInput!) { performAction(input: $input) { __typename ... on ActionSuccess { message timestamp } } }`, + vars: `{"variables":{"input":{"type":"success_only","payload":"test"}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + actionResult, ok := data["performAction"].(map[string]interface{}) + require.True(t, ok, "performAction should be an object") + require.NotEmpty(t, actionResult, "performAction should not be empty") + require.Contains(t, actionResult, "__typename") + require.Equal(t, "ActionSuccess", actionResult["__typename"]) + + require.Contains(t, actionResult, "message") + require.Contains(t, actionResult, "timestamp") + require.Equal(t, "Action 'success_only' completed successfully", actionResult["message"]) + require.Equal(t, "2024-01-01T00:00:00Z", actionResult["timestamp"]) + }, + }, + { + name: "Mutation perform action - only error fragment", + query: `mutation($input: ActionInput!) { performAction(input: $input) { __typename ... on ActionError { message code } } }`, + vars: `{"variables":{"input":{"type":"error_action","payload":"test"}}}`, + validate: func(t *testing.T, data map[string]interface{}) { + actionResult, ok := data["performAction"].(map[string]interface{}) + require.True(t, ok, "performAction should be an object") + require.NotEmpty(t, actionResult, "performAction should not be empty") + require.Contains(t, actionResult, "__typename") + require.Equal(t, "ActionError", actionResult["__typename"]) + + require.Contains(t, actionResult, "message") + require.Contains(t, actionResult, "code") + require.Equal(t, "Action failed due to validation error", actionResult["message"]) + require.Equal(t, "VALIDATION_ERROR", actionResult["code"]) + }, + }, } for _, tc := range testCases { @@ -766,6 +948,14 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { Response: "MutationPerformActionResponse", }, }, + EnumValues: map[string][]EnumValueMapping{ + "CategoryKind": { + {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, + {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, + {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, + {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, + }, + }, Fields: map[string]FieldMap{ "Query": { "randomSearchResult": { @@ -786,6 +976,36 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { }, }, }, + "Product": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "price": { + TargetName: "price", + }, + }, + "User": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "Category": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + }, "ActionSuccess": { "message": { TargetName: "message", From 73ea3eab416a8ef9c6cd64a33dd80cdca8f6653f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 1 Jul 2025 17:20:05 +0200 Subject: [PATCH 05/13] chore: fix error message --- v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index f353979e3b..1c00dfbef9 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -142,12 +142,12 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa if message.IsOneOf() { oneof := data.Descriptor().Oneofs().ByName(protoref.Name(message.OneOfType.FieldName())) if oneof == nil { - return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) + return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.OneOfType.FieldName(), message.Name) } oneofDescriptor := data.WhichOneof(oneof) if oneofDescriptor == nil { - return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.Name, message.Name) + return nil, fmt.Errorf("unable to build response JSON: oneof %s not found in message %s", message.OneOfType.FieldName(), message.Name) } if oneofDescriptor.Kind() == protoref.MessageKind { From 401f36834d1760919cbdedc2449358e3fd3597c1 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 11:36:53 +0200 Subject: [PATCH 06/13] chore: use smaller data type --- .../grpc_datasource/execution_plan.go | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index 60108d0e4c..73e3fea7bf 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -15,7 +15,17 @@ const ( // OneOfType represents the type of a oneof field in a protobuf message. // It can be either an interface or a union type. -type OneOfType int +type OneOfType uint8 + +// OneOfType constants define the different types of oneof fields. +const ( + // OneOfTypeNone represents no oneof type (default/zero value) + OneOfTypeNone OneOfType = 1 << iota + // OneOfTypeInterface represents an interface type oneof field + OneOfTypeInterface + // OneOfTypeUnion represents a union type oneof field + OneOfTypeUnion +) // FieldName returns the corresponding field name for the OneOfType. // For interfaces, it returns "instance", for unions it returns "value". @@ -31,16 +41,6 @@ func (o OneOfType) FieldName() string { return "" } -// OneOfType constants define the different types of oneof fields. -const ( - // OneOfTypeNone represents no oneof type (default/zero value) - OneOfTypeNone OneOfType = iota - // OneOfTypeInterface represents an interface type oneof field - OneOfTypeInterface - // OneOfTypeUnion represents a union type oneof field - OneOfTypeUnion -) - // RPCExecutionPlan represents a plan for executing one or more RPC calls // to gRPC services. It defines the sequence of calls and their dependencies. type RPCExecutionPlan struct { From 6ec337eaf1d350ccefcdf60c17ea4e06e987a8a2 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 14:33:44 +0200 Subject: [PATCH 07/13] chore: improve handling of inline fragments --- .../grpc_datasource/execution_plan.go | 34 +- .../grpc_datasource/execution_plan_test.go | 54 +++ .../grpc_datasource/grpc_datasource.go | 5 +- .../grpc_datasource/grpc_datasource_test.go | 369 ++++++++++++------ 4 files changed, 331 insertions(+), 131 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index 73e3fea7bf..c95cf879d7 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -87,6 +87,16 @@ func (r *RPCMessage) IsOneOf() bool { return r.OneOfType > OneOfTypeNone && r.OneOfType <= OneOfTypeUnion } +// SelectValidTypes returns the valid types for a given type name. +func (r *RPCMessage) SelectValidTypes(typeName string) []string { + if r.Name == typeName { + return []string{r.Name} + } + + // If we have an interface or union type, we need to select the provided type as well. + return []string{r.Name, typeName} +} + // RPCFieldSelectionSet is a map of field selections based on inline fragments type RPCFieldSelectionSet map[string]RPCFields @@ -99,11 +109,25 @@ func (r RPCFieldSelectionSet) Add(fragmentName string, field RPCField) { r[fragmentName] = append(r[fragmentName], field) } -// FieldsForSelection returns the fields for a given fragment name. -func (r RPCFieldSelectionSet) FieldsForSelection(fragmentName string) RPCFields { - fields, ok := r[fragmentName] - if !ok { - return nil +// SelectFieldsForTypes returns the fields for the given valid types. +// It also makes sure to deduplicate the fields. +func (r RPCFieldSelectionSet) SelectFieldsForTypes(validTypes []string) RPCFields { + fieldSet := make(map[string]struct{}) + fields := make(RPCFields, 0) + for _, typeName := range validTypes { + lookupFields, ok := r[typeName] + if !ok { + continue + } + + for _, field := range lookupFields { + if _, found := fieldSet[field.Name]; found { + continue + } + + fieldSet[field.Name] = struct{}{} + fields = append(fields, field) + } } return fields diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index 3319d9a0d1..cfef93fc75 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -1639,6 +1639,60 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { }, }, }, + { + name: "Should create an execution plan for a query with all pets using an interface fragment", + query: "query AllPetsQuery { allPets { ... on Animal { id name kind } } }", + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "QueryAllPets", + Request: RPCMessage{ + Name: "QueryAllPetsRequest", + }, + Response: RPCMessage{ + Name: "QueryAllPetsResponse", + Fields: []RPCField{ + { + Name: "all_pets", + TypeName: string(DataTypeMessage), + JSONPath: "allPets", + Repeated: true, + Message: &RPCMessage{ + Name: "Animal", + OneOfType: OneOfTypeInterface, + MemberTypes: []string{ + "Cat", + "Dog", + }, + Fields: RPCFields{}, + FieldSelectionSet: RPCFieldSelectionSet{ + "Animal": { + { + Name: "id", + TypeName: string(DataTypeString), + JSONPath: "id", + }, + { + Name: "name", + TypeName: string(DataTypeString), + JSONPath: "name", + }, + { + Name: "kind", + TypeName: string(DataTypeString), + JSONPath: "kind", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, { name: "Should create an execution plan for a query with interface selecting only common fields", query: "query CommonFieldsQuery { randomPet { id name kind } }", diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go index 1c00dfbef9..a87a266247 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go @@ -155,7 +155,10 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa } } - validFields := append(message.Fields, message.FieldSelectionSet.FieldsForSelection(string(data.Type().Descriptor().Name()))...) + validFields := message.Fields + if message.IsOneOf() { + validFields = append(validFields, message.FieldSelectionSet.SelectFieldsForTypes(message.SelectValidTypes(string(data.Type().Descriptor().Name())))...) + } for _, field := range validFields { if field.StaticValue != "" { diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index f7297fb541..7ef715345a 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -496,169 +496,288 @@ func TestMarshalResponseJSON(t *testing.T) { // Test_DataSource_Load_WithAnimalInterface tests the datasource with Animal interface types (Cat/Dog) // using a bufconn connection to the mock service func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { - // 1. Start a gRPC server with our mock implementation - lis, err := net.Listen("tcp", "localhost:0") - require.NoError(t, err) - - // Get the server address - serverAddr := fmt.Sprintf("localhost:%d", lis.Addr().(*net.TCPAddr).Port) + // Set up the bufconn listener + lis := bufconn.Listen(1024 * 1024) - // Create and start the gRPC server + // Create a new gRPC server server := grpc.NewServer() + + // Register our mock service implementation mockService := &grpctest.MockService{} productv1.RegisterProductServiceServer(server, mockService) + // Start the server in a goroutine go func() { if err := server.Serve(lis); err != nil { t.Errorf("failed to serve: %v", err) } }() + + // Clean up the server when the test completes defer server.Stop() - // 2. Connect to the gRPC server + // Create a buffer-based dialer + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + // Connect using bufconn dialer // see https://github.com/grpc/grpc-go/issues/7091 // nolint: staticcheck conn, err := grpc.Dial( - serverAddr, + "bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(bufDialer), grpc.WithLocalDNSResolution(), ) require.NoError(t, err) defer conn.Close() - // Define the GraphQL query for the Animal interface - query := `query RandomPetQuery { - randomPet { - __typename - id - name - kind - ... on Cat { - meowVolume - } - ... on Dog { - barkVolume - } - } - }` + testCases := []struct { + name string + query string + vars string + validate func(t *testing.T, data map[string]interface{}) + }{ + { + name: "Query random pet with only common fields", + query: `query RandomPetQuery { + randomPet { + __typename + id + name + kind + } + }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + randomPet, ok := data["randomPet"].(map[string]interface{}) + require.True(t, ok, "randomPet should be an object") + require.NotNil(t, randomPet, "RandomPet should not be nil") + + // Verify common fields + require.Contains(t, randomPet, "__typename") + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "name") + require.Contains(t, randomPet, "kind") + + // Verify __typename is either Cat or Dog + typename := randomPet["__typename"].(string) + require.Contains(t, []string{"Cat", "Dog"}, typename, "typename should be either Cat or Dog") + + // Verify specific fields are not present since they weren't requested + require.NotContains(t, randomPet, "meowVolume") + require.NotContains(t, randomPet, "barkVolume") + }, + }, + { + name: "Query random pet with full interface fields", + query: `query RandomPetQuery { + randomPet { + __typename + id + name + kind + ... on Cat { + meowVolume + } + ... on Dog { + barkVolume + } + } + }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + randomPet, ok := data["randomPet"].(map[string]interface{}) + require.True(t, ok, "randomPet should be an object") + require.NotNil(t, randomPet, "RandomPet should not be nil") + + // Check if we got either a cat or dog by checking for their specific fields + if _, hasCat := randomPet["meowVolume"]; hasCat { + // We got a Cat response + require.Contains(t, randomPet, "__typename") + require.Equal(t, "Cat", randomPet["__typename"]) + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "name") + require.Contains(t, randomPet, "kind") + require.Contains(t, randomPet, "meowVolume") + } else if _, hasDog := randomPet["barkVolume"]; hasDog { + // We got a Dog response + require.Contains(t, randomPet, "__typename") + require.Equal(t, "Dog", randomPet["__typename"]) + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "name") + require.Contains(t, randomPet, "kind") + require.Contains(t, randomPet, "barkVolume") + } else { + t.Fatalf("Response doesn't contain either a Cat or Dog type: %v", randomPet) + } + }, + }, + { + name: "Query random pet with only Cat fragment", + query: `query RandomPetQuery { + randomPet { + __typename + id + name + ... on Cat { + meowVolume + } + } + }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + randomPet, ok := data["randomPet"].(map[string]interface{}) + require.True(t, ok, "randomPet should be an object") + require.NotNil(t, randomPet, "RandomPet should not be nil") - schemaDoc := grpctest.MustGraphQLSchema(t) + // Common fields should always be present + require.Contains(t, randomPet, "__typename") + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "name") - queryDoc, report := astparser.ParseGraphqlDocumentString(query) - if report.HasErrors() { - t.Fatalf("failed to parse query: %s", report.Error()) - } + typename := randomPet["__typename"].(string) + require.Contains(t, []string{"Cat", "Dog"}, typename, "typename should be either Cat or Dog") - // Create mapping configuration based on the mapping.go - mapping := &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "randomPet": { - RPC: "QueryRandomPet", - Request: "QueryRandomPetRequest", - Response: "QueryRandomPetResponse", + // If it's a Cat, meowVolume should be present + if typename == "Cat" { + require.Contains(t, randomPet, "meowVolume") + } + // barkVolume should never be present since it wasn't requested + require.NotContains(t, randomPet, "barkVolume") }, }, - Fields: map[string]FieldMap{ - "Query": { - "randomPet": { - TargetName: "random_pet", - }, - }, - "Cat": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - "meowVolume": { - TargetName: "meow_volume", - }, + { + name: "Query random pet with only Animal fragment", + query: `query RandomPetQuery { + randomPet { + __typename + ... on Animal { + kind + } + } + }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + randomPet, ok := data["randomPet"].(map[string]interface{}) + require.True(t, ok, "randomPet should be an object") + require.NotNil(t, randomPet, "RandomPet should not be nil") + + // Common fields should always be present + require.Contains(t, randomPet, "__typename") + require.Contains(t, randomPet, "kind") + + typename := randomPet["__typename"].(string) + require.Contains(t, []string{"Cat", "Dog"}, typename, "typename should be either Cat or Dog") }, - "Dog": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - "barkVolume": { - TargetName: "bark_volume", - }, + }, + { + name: "Query random pet with Animal and Member fragments", + query: `query RandomPetQuery { + randomPet { + __typename + ... on Animal { + id + kind + } + ... on Cat { + id + meowVolume + } + ... on Dog { + id + name + barkVolume + } + } + }`, + vars: "{}", + validate: func(t *testing.T, data map[string]interface{}) { + randomPet, ok := data["randomPet"].(map[string]interface{}) + require.True(t, ok, "randomPet should be an object") + require.NotNil(t, randomPet, "RandomPet should not be nil") + + // Common fields should always be present + require.Contains(t, randomPet, "__typename") + require.Contains(t, randomPet, "kind") + + typename := randomPet["__typename"].(string) + require.Contains(t, []string{"Cat", "Dog"}, typename, "typename should be either Cat or Dog") + + switch typename { + case "Cat": + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "meowVolume") + require.Contains(t, randomPet, "kind") + require.NotContains(t, randomPet, "name") + require.NotContains(t, randomPet, "barkVolume") + + require.Equal(t, "cat-1", randomPet["id"]) + require.Equal(t, "Siamese", randomPet["kind"]) + case "Dog": + require.Contains(t, randomPet, "id") + require.Contains(t, randomPet, "name") + require.Contains(t, randomPet, "kind") + require.Contains(t, randomPet, "barkVolume") + require.NotContains(t, randomPet, "meowVolume") + + require.Equal(t, "dog-1", randomPet["id"]) + require.Equal(t, "Dalmatian", randomPet["kind"]) + require.Equal(t, "Spot", randomPet["name"]) + } }, }, } - compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) - if err != nil { - t.Fatalf("failed to compile proto: %v", err) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Parse the GraphQL schema + schemaDoc := grpctest.MustGraphQLSchema(t) - // Create the datasource - ds, err := NewDataSource(conn, DataSourceConfig{ - Operation: &queryDoc, - Definition: &schemaDoc, - SubgraphName: "Products", - Compiler: compiler, - Mapping: mapping, - }) - require.NoError(t, err) + // Parse the GraphQL query + queryDoc, report := astparser.ParseGraphqlDocumentString(tc.query) + if report.HasErrors() { + t.Fatalf("failed to parse query: %s", report.Error()) + } - // Execute the query through our datasource - output := new(bytes.Buffer) - err = ds.Load(context.Background(), []byte(`{"query":`+fmt.Sprintf("%q", query)+`}`), output) - require.NoError(t, err) + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping()) + if err != nil { + t.Fatalf("failed to compile proto: %v", err) + } - // Print the response for debugging - responseData := output.String() - t.Logf("Response: %s", responseData) + // Create the datasource + ds, err := NewDataSource(conn, DataSourceConfig{ + Operation: &queryDoc, + Definition: &schemaDoc, + SubgraphName: "Products", + Compiler: compiler, + Mapping: testMapping(), + }) + require.NoError(t, err) - // Define a response structure that can handle both Cat and Dog types - type response struct { - Data struct { - RandomPet map[string]interface{} `json:"randomPet"` - } `json:"data"` - Errors []struct { - Message string `json:"message"` - } `json:"errors,omitempty"` - } + // Execute the query through our datasource + output := new(bytes.Buffer) + input := fmt.Sprintf(`{"query":%q,"body":%s}`, tc.query, tc.vars) + err = ds.Load(context.Background(), []byte(input), output) + require.NoError(t, err) - var resp response - err = json.Unmarshal(output.Bytes(), &resp) - require.NoError(t, err, "Failed to unmarshal response") + // Parse the response + var resp struct { + Data map[string]interface{} `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors,omitempty"` + } - // Verify there are no errors - require.Empty(t, resp.Errors, "Response should not contain errors") + err = json.Unmarshal(output.Bytes(), &resp) + require.NoError(t, err, "Failed to unmarshal response") + require.Empty(t, resp.Errors, "Response should not contain errors") + require.NotEmpty(t, resp.Data, "Response should contain data") - // Verify we have data - require.NotNil(t, resp.Data.RandomPet, "RandomPet should not be nil") - - // Check if we got either a cat or dog by checking for their specific fields - if _, hasCat := resp.Data.RandomPet["meowVolume"]; hasCat { - // We got a Cat response - require.Contains(t, resp.Data.RandomPet, "__typename") - require.Equal(t, "Cat", resp.Data.RandomPet["__typename"]) - require.Contains(t, resp.Data.RandomPet, "id") - require.Contains(t, resp.Data.RandomPet, "name") - require.Contains(t, resp.Data.RandomPet, "kind") - require.Contains(t, resp.Data.RandomPet, "meowVolume") - } else if _, hasDog := resp.Data.RandomPet["barkVolume"]; hasDog { - // We got a Dog response - require.Contains(t, resp.Data.RandomPet, "__typename") - require.Equal(t, "Dog", resp.Data.RandomPet["__typename"]) - require.Contains(t, resp.Data.RandomPet, "id") - require.Contains(t, resp.Data.RandomPet, "name") - require.Contains(t, resp.Data.RandomPet, "kind") - require.Contains(t, resp.Data.RandomPet, "barkVolume") - } else { - t.Fatalf("Response doesn't contain either a Cat or Dog type: %v", resp.Data.RandomPet) + // Run the validation function + tc.validate(t, resp.Data) + }) } } From 6591f6c189e0c7c4504a14c6aa9f48d066f249fa Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 15:42:58 +0200 Subject: [PATCH 08/13] chore: cleanup test file --- .../grpc_datasource/grpc_datasource_test.go | 365 ++++-------------- 1 file changed, 73 insertions(+), 292 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index 7ef715345a..007fb8dacb 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -53,6 +53,51 @@ func (m mockInterface) NewStream(ctx context.Context, desc *grpc.StreamDesc, met var _ grpc.ClientConnInterface = (*mockInterface)(nil) +func setupTestGRPCServer(t *testing.T) (conn *grpc.ClientConn, cleanup func()) { + t.Helper() + + // Set up the bufconn listener + lis := bufconn.Listen(1024 * 1024) + + // Create a new gRPC server + server := grpc.NewServer() + + // Register our mock service implementation + mockService := &grpctest.MockService{} + productv1.RegisterProductServiceServer(server, mockService) + + // Start the server in a goroutine + go func() { + if err := server.Serve(lis); err != nil { + t.Errorf("failed to serve: %v", err) + } + }() + + // Create a buffer-based dialer + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + // Connect using bufconn dialer + // see https://github.com/grpc/grpc-go/issues/7091 + // nolint: staticcheck + conn, err := grpc.Dial( + "bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(bufDialer), + grpc.WithLocalDNSResolution(), + ) + require.NoError(t, err) + + cleanup = func() { + conn.Close() + server.Stop() + lis.Close() + } + + return conn, cleanup +} + // Test_DataSource_Load tests the datasource.Load method with a mock gRPC interface func Test_DataSource_Load(t *testing.T) { query := `query ComplexFilterTypeQuery($filter: ComplexFilterTypeInput!) { complexFilterType(filter: $filter) { id name } }` @@ -102,44 +147,10 @@ func Test_DataSource_Load(t *testing.T) { // Test_DataSource_Load_WithMockService tests the datasource.Load method with an actual gRPC server // TODO update this test to not use mappings anc expect no response func Test_DataSource_Load_WithMockService(t *testing.T) { - // 1. Start a real gRPC server with our mock implementation - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - ) - - require.NoError(t, err) - defer conn.Close() - - // 3. Set up GraphQL query and schema + // 1. Set up GraphQL query and schema query := `query ComplexFilterTypeQuery($filter: ComplexFilterTypeInput!) { complexFilterType(filter: $filter) { id name } }` variables := `{"variables":{"filter":{"filter":{"name":"Test Product","filterField1":"filterField1","filterField2":"filterField2"}}}}` @@ -157,7 +168,7 @@ func Test_DataSource_Load_WithMockService(t *testing.T) { t.Fatalf("failed to compile proto: %v", err) } - // 4. Create a datasource with the real gRPC client connection + // 2. Create a datasource with the real gRPC client connection ds, err := NewDataSource(conn, DataSourceConfig{ Operation: &queryDoc, Definition: &schemaDoc, @@ -186,7 +197,7 @@ func Test_DataSource_Load_WithMockService(t *testing.T) { }) require.NoError(t, err) - // 5. Execute the query through our datasource + // 3. Execute the query through our datasource output := new(bytes.Buffer) err = ds.Load(context.Background(), []byte(`{"query":"`+query+`","body":`+variables+`}`), output) require.NoError(t, err) @@ -216,44 +227,10 @@ func Test_DataSource_Load_WithMockService(t *testing.T) { } func Test_DataSource_Load_WithMockService_WithResponseMapping(t *testing.T) { - // 1. Start a real gRPC server with our mock implementation - lis := bufconn.Listen(1024 * 1024) + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() - - // 3. Set up GraphQL query and schema + // 1. Set up GraphQL query and schema query := `query ComplexFilterTypeQuery($filter: ComplexFilterTypeInput!) { complexFilterType(filter: $filter) { id name } }` variables := `{"variables":{"filter":{"filter":{"name":"HARDCODED_NAME_TEST","filterField1":"value1","filterField2":"value2"}}}}` @@ -270,7 +247,7 @@ func Test_DataSource_Load_WithMockService_WithResponseMapping(t *testing.T) { t.Fatalf("failed to compile proto: %v", err) } - // 4. Create a datasource with the real gRPC client connection + // 2. Create a datasource with the real gRPC client connection ds, err := NewDataSource(conn, DataSourceConfig{ Operation: &queryDoc, Definition: &schemaDoc, @@ -299,7 +276,7 @@ func Test_DataSource_Load_WithMockService_WithResponseMapping(t *testing.T) { }) require.NoError(t, err) - // 5. Execute the query through our datasource + // 3. Execute the query through our datasource output := new(bytes.Buffer) // Format the input with query and variables @@ -342,41 +319,14 @@ func Test_DataSource_Load_WithMockService_WithResponseMapping(t *testing.T) { // Test_DataSource_Load_WithGrpcError tests how the datasource handles gRPC errors // and formats them as GraphQL errors in the response func Test_DataSource_Load_WithGrpcError(t *testing.T) { - // 1. Start a gRPC server with our mock implementation - lis, err := net.Listen("tcp", "localhost:0") - require.NoError(t, err) - - // Get the server address - serverAddr := fmt.Sprintf("localhost:%d", lis.Addr().(*net.TCPAddr).Port) - - // Create and start the gRPC server - server := grpc.NewServer() - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - defer server.Stop() - - // 2. Connect to the gRPC server - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - serverAddr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() - - // 3. Set up the GraphQL query that will trigger the error + // 1. Set up the GraphQL query that will trigger the error query := `query UserQuery($id: ID!) { user(id: $id) { id name } }` variables := `{"variables":{"id":"error-user"}}` - // 4. Parse the schema and query + // 2. Parse the schema and query schemaDoc := grpctest.MustGraphQLSchema(t) queryDoc, report := astparser.ParseGraphqlDocumentString(query) @@ -389,7 +339,7 @@ func Test_DataSource_Load_WithGrpcError(t *testing.T) { t.Fatalf("failed to compile proto: %v", err) } - // 5. Create the datasource + // 3. Create the datasource ds, err := NewDataSource(conn, DataSourceConfig{ Operation: &queryDoc, Definition: &schemaDoc, @@ -398,19 +348,19 @@ func Test_DataSource_Load_WithGrpcError(t *testing.T) { }) require.NoError(t, err) - // 6. Execute the query + // 4. Execute the query output := new(bytes.Buffer) err = ds.Load(context.Background(), []byte(`{"query":"`+query+`","body":`+variables+`}`), output) require.NoError(t, err, "Load should not return an error even when the gRPC call fails") responseJson := output.String() - // 7. Verify the response format according to GraphQL specification + // 5. Verify the response format according to GraphQL specification // The response should have an "errors" array with the error message require.Contains(t, responseJson, "errors") require.Contains(t, responseJson, "user not found: error-user") - // 8. Parse the response JSON for more detailed validation + // 6. Parse the response JSON for more detailed validation var response struct { Errors []struct { Message string `json:"message"` @@ -496,42 +446,8 @@ func TestMarshalResponseJSON(t *testing.T) { // Test_DataSource_Load_WithAnimalInterface tests the datasource with Animal interface types (Cat/Dog) // using a bufconn connection to the mock service func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { - // Set up the bufconn listener - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) testCases := []struct { name string @@ -782,42 +698,8 @@ func Test_DataSource_Load_WithAnimalInterface(t *testing.T) { } func Test_Datasource_Load_WithUnionTypes(t *testing.T) { - // Set up the bufconn listener - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) testCases := []struct { name string @@ -1203,42 +1085,8 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { // Test_DataSource_Load_WithProductQueries tests the product-related query operations // Category queries are used to mainly focus on testing Enum values func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { - // Set up the bufconn listener - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) // Define test cases testCases := []struct { @@ -1450,42 +1298,9 @@ func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { // Test_DataSource_Load_WithTotalCalculation tests the calculation of order totals using the // MockService implementation func Test_DataSource_Load_WithTotalCalculation(t *testing.T) { - // Set up the bufconn listener - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) // Define the GraphQL query query := ` @@ -1664,42 +1479,8 @@ func Test_DataSource_Load_WithTotalCalculation(t *testing.T) { // Test_DataSource_Load_WithTypename tests that __typename fields are correctly included // in the response with their static values func Test_DataSource_Load_WithTypename(t *testing.T) { - // Set up the bufconn listener - lis := bufconn.Listen(1024 * 1024) - - // Create a new gRPC server - server := grpc.NewServer() - - // Register our mock service implementation - mockService := &grpctest.MockService{} - productv1.RegisterProductServiceServer(server, mockService) - - // Start the server in a goroutine - go func() { - if err := server.Serve(lis); err != nil { - t.Errorf("failed to serve: %v", err) - } - }() - - // Clean up the server when the test completes - defer server.Stop() - - // Create a buffer-based dialer - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } - - // Connect using bufconn dialer - // see https://github.com/grpc/grpc-go/issues/7091 - // nolint: staticcheck - conn, err := grpc.Dial( - "bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(bufDialer), - grpc.WithLocalDNSResolution(), - ) - require.NoError(t, err) - defer conn.Close() + conn, cleanup := setupTestGRPCServer(t) + t.Cleanup(cleanup) // Define GraphQL query that requests __typename query := `query UsersWithTypename { users { __typename id name } }` From d3c24f614d0b76b779990943116351b56e7ac815 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 15:45:02 +0200 Subject: [PATCH 09/13] chore: remove inline mapping --- .../grpc_datasource/grpc_datasource_test.go | 299 +----------------- 1 file changed, 8 insertions(+), 291 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go index 007fb8dacb..71bf396635 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go @@ -928,121 +928,7 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { t.Fatalf("failed to parse query: %s", report.Error()) } - mapping := &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "randomSearchResult": { - RPC: "QueryRandomSearchResult", - Request: "QueryRandomSearchResultRequest", - Response: "QueryRandomSearchResultResponse", - }, - "search": { - RPC: "QuerySearch", - Request: "QuerySearchRequest", - Response: "QuerySearchResponse", - }, - }, - MutationRPCs: map[string]RPCConfig{ - "performAction": { - RPC: "MutationPerformAction", - Request: "MutationPerformActionRequest", - Response: "MutationPerformActionResponse", - }, - }, - EnumValues: map[string][]EnumValueMapping{ - "CategoryKind": { - {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, - {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, - {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, - {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "randomSearchResult": { - TargetName: "random_search_result", - }, - "search": { - TargetName: "search", - ArgumentMappings: map[string]string{ - "input": "input", - }, - }, - }, - "Mutation": { - "performAction": { - TargetName: "perform_action", - ArgumentMappings: map[string]string{ - "input": "input", - }, - }, - }, - "Product": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "price": { - TargetName: "price", - }, - }, - "User": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - "ActionSuccess": { - "message": { - TargetName: "message", - }, - "timestamp": { - TargetName: "timestamp", - }, - }, - "ActionError": { - "message": { - TargetName: "message", - }, - "code": { - TargetName: "code", - }, - }, - "SearchInput": { - "query": { - TargetName: "query", - }, - "limit": { - TargetName: "limit", - }, - }, - "ActionInput": { - "type": { - TargetName: "type", - }, - "payload": { - TargetName: "payload", - }, - }, - }, - } - - compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping()) if err != nil { t.Fatalf("failed to compile proto: %v", err) } @@ -1052,7 +938,7 @@ func Test_Datasource_Load_WithUnionTypes(t *testing.T) { Operation: &queryDoc, Definition: &schemaDoc, SubgraphName: "Products", - Mapping: mapping, + Mapping: testMapping(), Compiler: compiler, }) require.NoError(t, err) @@ -1179,83 +1065,7 @@ func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { t.Fatalf("failed to parse query: %s", report.Error()) } - // Create a new GRPCMapping configuration - mapping := &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "categories": { - RPC: "QueryCategories", - Request: "QueryCategoriesRequest", - Response: "QueryCategoriesResponse", - }, - "categoriesByKind": { - RPC: "QueryCategoriesByKind", - Request: "QueryCategoriesByKindRequest", - Response: "QueryCategoriesByKindResponse", - }, - "filterCategories": { - RPC: "QueryFilterCategories", - Request: "QueryFilterCategoriesRequest", - Response: "QueryFilterCategoriesResponse", - }, - }, - EnumValues: map[string][]EnumValueMapping{ - "CategoryKind": { - {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, - {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, - {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, - {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "categories": { - TargetName: "categories", - }, - "categoriesByKind": { - TargetName: "categories_by_kind", - ArgumentMappings: map[string]string{ - "kind": "kind", - }, - }, - "filterCategories": { - TargetName: "filter_categories", - ArgumentMappings: map[string]string{ - "filter": "filter", - }, - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - "CategoryFilter": { - "category": { - TargetName: "category", - }, - "pagination": { - TargetName: "pagination", - }, - }, - "Pagination": { - "page": { - TargetName: "page", - }, - "perPage": { - TargetName: "per_page", - }, - }, - }, - } - - compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping()) if err != nil { t.Fatalf("failed to compile proto: %v", err) } @@ -1265,7 +1075,7 @@ func Test_DataSource_Load_WithCategoryQueries(t *testing.T) { Operation: &queryDoc, Definition: &schemaDoc, SubgraphName: "Products", - Mapping: mapping, + Mapping: testMapping(), Compiler: compiler, }) require.NoError(t, err) @@ -1337,73 +1147,7 @@ func Test_DataSource_Load_WithTotalCalculation(t *testing.T) { t.Fatalf("failed to parse query: %s", report.Error()) } - // Create mapping configuration - mapping := &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "calculateTotals": { - RPC: "QueryCalculateTotals", - Request: "QueryCalculateTotalsRequest", - Response: "QueryCalculateTotalsResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "calculateTotals": { - TargetName: "calculate_totals", - }, - }, - "Order": { - "orderId": { - TargetName: "order_id", - }, - "customerName": { - TargetName: "customer_name", - }, - "totalItems": { - TargetName: "total_items", - }, - "orderLines": { - TargetName: "order_lines", - }, - }, - "OrderLine": { - "productId": { - TargetName: "product_id", - }, - "quantity": { - TargetName: "quantity", - }, - "modifiers": { - TargetName: "modifiers", - }, - }, - "OrderInput": { - "orderId": { - TargetName: "order_id", - }, - "customerName": { - TargetName: "customer_name", - }, - "lines": { - TargetName: "lines", - }, - }, - "OrderLineInput": { - "productId": { - TargetName: "product_id", - }, - "quantity": { - TargetName: "quantity", - }, - "modifiers": { - TargetName: "modifiers", - }, - }, - }, - } - - compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping()) if err != nil { t.Fatalf("failed to compile proto: %v", err) } @@ -1413,7 +1157,7 @@ func Test_DataSource_Load_WithTotalCalculation(t *testing.T) { Operation: &queryDoc, Definition: &schemaDoc, SubgraphName: "Products", - Mapping: mapping, + Mapping: testMapping(), Compiler: compiler, }) require.NoError(t, err) @@ -1494,34 +1238,7 @@ func Test_DataSource_Load_WithTypename(t *testing.T) { t.Fatalf("failed to parse query: %s", report.Error()) } - // Create a new GRPCMapping configuration - mapping := &GRPCMapping{ - Service: "ProductService", - QueryRPCs: map[string]RPCConfig{ - "users": { - RPC: "QueryUsers", - Request: "QueryUsersRequest", - Response: "QueryUsersResponse", - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "users": { - TargetName: "users", - }, - }, - "User": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - }, - } - - compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), mapping) + compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping()) if err != nil { t.Fatalf("failed to compile proto: %v", err) } @@ -1531,7 +1248,7 @@ func Test_DataSource_Load_WithTypename(t *testing.T) { Operation: &queryDoc, Definition: &schemaDoc, SubgraphName: "Products", - Mapping: mapping, + Mapping: testMapping(), Compiler: compiler, }) require.NoError(t, err) From c01c32679208910f101d2d7c988bea2fdba76e49 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 15:48:07 +0200 Subject: [PATCH 10/13] chore: move mapping to shared file --- .../grpc_datasource/execution_plan_test.go | 496 ----------------- .../grpc_datasource/mapping_test_helper.go | 497 ++++++++++++++++++ 2 files changed, 497 insertions(+), 496 deletions(-) create mode 100644 v2/pkg/engine/datasource/grpc_datasource/mapping_test_helper.go diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index cfef93fc75..a1b380819a 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -2576,499 +2576,3 @@ func TestProductExecutionPlan(t *testing.T) { }) } } - -func testMapping() *GRPCMapping { - return &GRPCMapping{ - Service: "Products", - QueryRPCs: map[string]RPCConfig{ - "users": { - RPC: "QueryUsers", - Request: "QueryUsersRequest", - Response: "QueryUsersResponse", - }, - "user": { - RPC: "QueryUser", - Request: "QueryUserRequest", - Response: "QueryUserResponse", - }, - "nestedType": { - RPC: "QueryNestedType", - Request: "QueryNestedTypeRequest", - Response: "QueryNestedTypeResponse", - }, - "recursiveType": { - RPC: "QueryRecursiveType", - Request: "QueryRecursiveTypeRequest", - Response: "QueryRecursiveTypeResponse", - }, - "typeFilterWithArguments": { - RPC: "QueryTypeFilterWithArguments", - Request: "QueryTypeFilterWithArgumentsRequest", - Response: "QueryTypeFilterWithArgumentsResponse", - }, - "typeWithMultipleFilterFields": { - RPC: "QueryTypeWithMultipleFilterFields", - Request: "QueryTypeWithMultipleFilterFieldsRequest", - Response: "QueryTypeWithMultipleFilterFieldsResponse", - }, - "complexFilterType": { - RPC: "QueryComplexFilterType", - Request: "QueryComplexFilterTypeRequest", - Response: "QueryComplexFilterTypeResponse", - }, - "calculateTotals": { - RPC: "QueryCalculateTotals", - Request: "QueryCalculateTotalsRequest", - Response: "QueryCalculateTotalsResponse", - }, - "randomPet": { - RPC: "QueryRandomPet", - Request: "QueryRandomPetRequest", - Response: "QueryRandomPetResponse", - }, - "allPets": { - RPC: "QueryAllPets", - Request: "QueryAllPetsRequest", - Response: "QueryAllPetsResponse", - }, - "categories": { - RPC: "QueryCategories", - Request: "QueryCategoriesRequest", - Response: "QueryCategoriesResponse", - }, - "categoriesByKind": { - RPC: "QueryCategoriesByKind", - Request: "QueryCategoriesByKindRequest", - Response: "QueryCategoriesByKindResponse", - }, - "categoriesByKinds": { - RPC: "QueryCategoriesByKinds", - Request: "QueryCategoriesByKindsRequest", - Response: "QueryCategoriesByKindsResponse", - }, - "filterCategories": { - RPC: "QueryFilterCategories", - Request: "QueryFilterCategoriesRequest", - Response: "QueryFilterCategoriesResponse", - }, - "randomSearchResult": { - RPC: "QueryRandomSearchResult", - Request: "QueryRandomSearchResultRequest", - Response: "QueryRandomSearchResultResponse", - }, - "search": { - RPC: "QuerySearch", - Request: "QuerySearchRequest", - Response: "QuerySearchResponse", - }, - }, - MutationRPCs: RPCConfigMap{ - "createUser": { - RPC: "CreateUser", - Request: "CreateUserRequest", - Response: "CreateUserResponse", - }, - "performAction": { - RPC: "MutationPerformAction", - Request: "MutationPerformActionRequest", - Response: "MutationPerformActionResponse", - }, - }, - SubscriptionRPCs: RPCConfigMap{}, - EntityRPCs: map[string]EntityRPCConfig{ - "Product": { - Key: "id", - RPCConfig: RPCConfig{ - RPC: "LookupProductById", - Request: "LookupProductByIdRequest", - Response: "LookupProductByIdResponse", - }, - }, - "Storage": { - Key: "id", - RPCConfig: RPCConfig{ - RPC: "LookupStorageById", - Request: "LookupStorageByIdRequest", - Response: "LookupStorageByIdResponse", - }, - }, - }, - EnumValues: map[string][]EnumValueMapping{ - "CategoryKind": { - {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, - {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, - {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, - {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, - }, - }, - Fields: map[string]FieldMap{ - "Query": { - "user": { - TargetName: "user", - ArgumentMappings: map[string]string{ - "id": "id", - }, - }, - "nestedType": { - TargetName: "nested_type", - }, - "recursiveType": { - TargetName: "recursive_type", - }, - "randomPet": { - TargetName: "random_pet", - }, - "allPets": { - TargetName: "all_pets", - }, - "categories": { - TargetName: "categories", - }, - "categoriesByKind": { - TargetName: "categories_by_kind", - ArgumentMappings: map[string]string{ - "kind": "kind", - }, - }, - "categoriesByKinds": { - TargetName: "categories_by_kinds", - ArgumentMappings: map[string]string{ - "kinds": "kinds", - }, - }, - "filterCategories": { - TargetName: "filter_categories", - ArgumentMappings: map[string]string{ - "filter": "filter", - }, - }, - "typeFilterWithArguments": { - TargetName: "type_filter_with_arguments", - ArgumentMappings: map[string]string{ - "filterField1": "filter_field_1", - "filterField2": "filter_field_2", - }, - }, - "typeWithMultipleFilterFields": { - TargetName: "type_with_multiple_filter_fields", - ArgumentMappings: map[string]string{ - "filter": "filter", - }, - }, - "complexFilterType": { - TargetName: "complex_filter_type", - ArgumentMappings: map[string]string{ - "filter": "filter", - }, - }, - "calculateTotals": { - TargetName: "calculate_totals", - ArgumentMappings: map[string]string{ - "orders": "orders", - }, - }, - "search": { - TargetName: "search", - ArgumentMappings: map[string]string{ - "input": "input", - }, - }, - "randomSearchResult": { - TargetName: "random_search_result", - }, - }, - "Mutation": { - "createUser": { - TargetName: "create_user", - ArgumentMappings: map[string]string{ - "input": "input", - }, - }, - "performAction": { - TargetName: "perform_action", - ArgumentMappings: map[string]string{ - "input": "input", - }, - }, - }, - "UserInput": { - "name": { - TargetName: "name", - }, - }, - "Product": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "price": { - TargetName: "price", - }, - }, - "Storage": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "location": { - TargetName: "location", - }, - }, - "User": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - "NestedTypeA": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "b": { - TargetName: "b", - }, - }, - "NestedTypeB": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "c": { - TargetName: "c", - }, - }, - "NestedTypeC": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - "RecursiveType": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "recursiveType": { - TargetName: "recursive_type", - }, - }, - "TypeWithMultipleFilterFields": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "filterField1": { - TargetName: "filter_field_1", - }, - "filterField2": { - TargetName: "filter_field_2", - }, - }, - "TypeWithComplexFilterInput": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - }, - "Cat": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - "meowVolume": { - TargetName: "meow_volume", - }, - }, - "Dog": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - "barkVolume": { - TargetName: "bark_volume", - }, - }, - "Animal": { - "cat": { - TargetName: "cat", - }, - "dog": { - TargetName: "dog", - }, - }, - "FilterType": { - "name": { - TargetName: "name", - }, - "filterField1": { - TargetName: "filter_field_1", - }, - "filterField2": { - TargetName: "filter_field_2", - }, - "pagination": { - TargetName: "pagination", - }, - }, - "Pagination": { - "page": { - TargetName: "page", - }, - "perPage": { - TargetName: "per_page", - }, - }, - "ComplexFilterTypeInput": { - "filter": { - TargetName: "filter", - }, - }, - "Category": { - "id": { - TargetName: "id", - }, - "name": { - TargetName: "name", - }, - "kind": { - TargetName: "kind", - }, - }, - "CategoryFilter": { - "category": { - TargetName: "category", - }, - "pagination": { - TargetName: "pagination", - }, - }, - "Order": { - "orderId": { - TargetName: "order_id", - }, - "customerName": { - TargetName: "customer_name", - }, - "totalItems": { - TargetName: "total_items", - }, - "orderLines": { - TargetName: "order_lines", - }, - }, - "OrderLine": { - "productId": { - TargetName: "product_id", - }, - "quantity": { - TargetName: "quantity", - }, - "modifiers": { - TargetName: "modifiers", - }, - }, - "OrderInput": { - "orderId": { - TargetName: "order_id", - }, - "customerName": { - TargetName: "customer_name", - }, - "lines": { - TargetName: "lines", - }, - }, - "OrderLineInput": { - "productId": { - TargetName: "product_id", - }, - "quantity": { - TargetName: "quantity", - }, - "modifiers": { - TargetName: "modifiers", - }, - }, - "ActionSuccess": { - "message": { - TargetName: "message", - }, - "timestamp": { - TargetName: "timestamp", - }, - }, - "ActionError": { - "message": { - TargetName: "message", - }, - "code": { - TargetName: "code", - }, - }, - "SearchInput": { - "query": { - TargetName: "query", - }, - "limit": { - TargetName: "limit", - }, - }, - "ActionInput": { - "type": { - TargetName: "type", - }, - "payload": { - TargetName: "payload", - }, - }, - "SearchResult": { - "product": { - TargetName: "product", - }, - "user": { - TargetName: "user", - }, - "category": { - TargetName: "category", - }, - }, - "ActionResult": { - "actionSuccess": { - TargetName: "action_success", - }, - "actionError": { - TargetName: "action_error", - }, - }, - }, - } -} diff --git a/v2/pkg/engine/datasource/grpc_datasource/mapping_test_helper.go b/v2/pkg/engine/datasource/grpc_datasource/mapping_test_helper.go new file mode 100644 index 0000000000..10313fe474 --- /dev/null +++ b/v2/pkg/engine/datasource/grpc_datasource/mapping_test_helper.go @@ -0,0 +1,497 @@ +package grpcdatasource + +func testMapping() *GRPCMapping { + return &GRPCMapping{ + Service: "Products", + QueryRPCs: map[string]RPCConfig{ + "users": { + RPC: "QueryUsers", + Request: "QueryUsersRequest", + Response: "QueryUsersResponse", + }, + "user": { + RPC: "QueryUser", + Request: "QueryUserRequest", + Response: "QueryUserResponse", + }, + "nestedType": { + RPC: "QueryNestedType", + Request: "QueryNestedTypeRequest", + Response: "QueryNestedTypeResponse", + }, + "recursiveType": { + RPC: "QueryRecursiveType", + Request: "QueryRecursiveTypeRequest", + Response: "QueryRecursiveTypeResponse", + }, + "typeFilterWithArguments": { + RPC: "QueryTypeFilterWithArguments", + Request: "QueryTypeFilterWithArgumentsRequest", + Response: "QueryTypeFilterWithArgumentsResponse", + }, + "typeWithMultipleFilterFields": { + RPC: "QueryTypeWithMultipleFilterFields", + Request: "QueryTypeWithMultipleFilterFieldsRequest", + Response: "QueryTypeWithMultipleFilterFieldsResponse", + }, + "complexFilterType": { + RPC: "QueryComplexFilterType", + Request: "QueryComplexFilterTypeRequest", + Response: "QueryComplexFilterTypeResponse", + }, + "calculateTotals": { + RPC: "QueryCalculateTotals", + Request: "QueryCalculateTotalsRequest", + Response: "QueryCalculateTotalsResponse", + }, + "randomPet": { + RPC: "QueryRandomPet", + Request: "QueryRandomPetRequest", + Response: "QueryRandomPetResponse", + }, + "allPets": { + RPC: "QueryAllPets", + Request: "QueryAllPetsRequest", + Response: "QueryAllPetsResponse", + }, + "categories": { + RPC: "QueryCategories", + Request: "QueryCategoriesRequest", + Response: "QueryCategoriesResponse", + }, + "categoriesByKind": { + RPC: "QueryCategoriesByKind", + Request: "QueryCategoriesByKindRequest", + Response: "QueryCategoriesByKindResponse", + }, + "categoriesByKinds": { + RPC: "QueryCategoriesByKinds", + Request: "QueryCategoriesByKindsRequest", + Response: "QueryCategoriesByKindsResponse", + }, + "filterCategories": { + RPC: "QueryFilterCategories", + Request: "QueryFilterCategoriesRequest", + Response: "QueryFilterCategoriesResponse", + }, + "randomSearchResult": { + RPC: "QueryRandomSearchResult", + Request: "QueryRandomSearchResultRequest", + Response: "QueryRandomSearchResultResponse", + }, + "search": { + RPC: "QuerySearch", + Request: "QuerySearchRequest", + Response: "QuerySearchResponse", + }, + }, + MutationRPCs: RPCConfigMap{ + "createUser": { + RPC: "CreateUser", + Request: "CreateUserRequest", + Response: "CreateUserResponse", + }, + "performAction": { + RPC: "MutationPerformAction", + Request: "MutationPerformActionRequest", + Response: "MutationPerformActionResponse", + }, + }, + SubscriptionRPCs: RPCConfigMap{}, + EntityRPCs: map[string]EntityRPCConfig{ + "Product": { + Key: "id", + RPCConfig: RPCConfig{ + RPC: "LookupProductById", + Request: "LookupProductByIdRequest", + Response: "LookupProductByIdResponse", + }, + }, + "Storage": { + Key: "id", + RPCConfig: RPCConfig{ + RPC: "LookupStorageById", + Request: "LookupStorageByIdRequest", + Response: "LookupStorageByIdResponse", + }, + }, + }, + EnumValues: map[string][]EnumValueMapping{ + "CategoryKind": { + {Value: "BOOK", TargetValue: "CATEGORY_KIND_BOOK"}, + {Value: "ELECTRONICS", TargetValue: "CATEGORY_KIND_ELECTRONICS"}, + {Value: "FURNITURE", TargetValue: "CATEGORY_KIND_FURNITURE"}, + {Value: "OTHER", TargetValue: "CATEGORY_KIND_OTHER"}, + }, + }, + Fields: map[string]FieldMap{ + "Query": { + "user": { + TargetName: "user", + ArgumentMappings: map[string]string{ + "id": "id", + }, + }, + "nestedType": { + TargetName: "nested_type", + }, + "recursiveType": { + TargetName: "recursive_type", + }, + "randomPet": { + TargetName: "random_pet", + }, + "allPets": { + TargetName: "all_pets", + }, + "categories": { + TargetName: "categories", + }, + "categoriesByKind": { + TargetName: "categories_by_kind", + ArgumentMappings: map[string]string{ + "kind": "kind", + }, + }, + "categoriesByKinds": { + TargetName: "categories_by_kinds", + ArgumentMappings: map[string]string{ + "kinds": "kinds", + }, + }, + "filterCategories": { + TargetName: "filter_categories", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "typeFilterWithArguments": { + TargetName: "type_filter_with_arguments", + ArgumentMappings: map[string]string{ + "filterField1": "filter_field_1", + "filterField2": "filter_field_2", + }, + }, + "typeWithMultipleFilterFields": { + TargetName: "type_with_multiple_filter_fields", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "complexFilterType": { + TargetName: "complex_filter_type", + ArgumentMappings: map[string]string{ + "filter": "filter", + }, + }, + "calculateTotals": { + TargetName: "calculate_totals", + ArgumentMappings: map[string]string{ + "orders": "orders", + }, + }, + "search": { + TargetName: "search", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + "randomSearchResult": { + TargetName: "random_search_result", + }, + }, + "Mutation": { + "createUser": { + TargetName: "create_user", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + "performAction": { + TargetName: "perform_action", + ArgumentMappings: map[string]string{ + "input": "input", + }, + }, + }, + "UserInput": { + "name": { + TargetName: "name", + }, + }, + "Product": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "price": { + TargetName: "price", + }, + }, + "Storage": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "location": { + TargetName: "location", + }, + }, + "User": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "NestedTypeA": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "b": { + TargetName: "b", + }, + }, + "NestedTypeB": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "c": { + TargetName: "c", + }, + }, + "NestedTypeC": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "RecursiveType": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "recursiveType": { + TargetName: "recursive_type", + }, + }, + "TypeWithMultipleFilterFields": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "filterField1": { + TargetName: "filter_field_1", + }, + "filterField2": { + TargetName: "filter_field_2", + }, + }, + "TypeWithComplexFilterInput": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + }, + "Cat": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "meowVolume": { + TargetName: "meow_volume", + }, + }, + "Dog": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + "barkVolume": { + TargetName: "bark_volume", + }, + }, + "Animal": { + "cat": { + TargetName: "cat", + }, + "dog": { + TargetName: "dog", + }, + }, + "FilterType": { + "name": { + TargetName: "name", + }, + "filterField1": { + TargetName: "filter_field_1", + }, + "filterField2": { + TargetName: "filter_field_2", + }, + "pagination": { + TargetName: "pagination", + }, + }, + "Pagination": { + "page": { + TargetName: "page", + }, + "perPage": { + TargetName: "per_page", + }, + }, + "ComplexFilterTypeInput": { + "filter": { + TargetName: "filter", + }, + }, + "Category": { + "id": { + TargetName: "id", + }, + "name": { + TargetName: "name", + }, + "kind": { + TargetName: "kind", + }, + }, + "CategoryFilter": { + "category": { + TargetName: "category", + }, + "pagination": { + TargetName: "pagination", + }, + }, + "Order": { + "orderId": { + TargetName: "order_id", + }, + "customerName": { + TargetName: "customer_name", + }, + "totalItems": { + TargetName: "total_items", + }, + "orderLines": { + TargetName: "order_lines", + }, + }, + "OrderLine": { + "productId": { + TargetName: "product_id", + }, + "quantity": { + TargetName: "quantity", + }, + "modifiers": { + TargetName: "modifiers", + }, + }, + "OrderInput": { + "orderId": { + TargetName: "order_id", + }, + "customerName": { + TargetName: "customer_name", + }, + "lines": { + TargetName: "lines", + }, + }, + "OrderLineInput": { + "productId": { + TargetName: "product_id", + }, + "quantity": { + TargetName: "quantity", + }, + "modifiers": { + TargetName: "modifiers", + }, + }, + "ActionSuccess": { + "message": { + TargetName: "message", + }, + "timestamp": { + TargetName: "timestamp", + }, + }, + "ActionError": { + "message": { + TargetName: "message", + }, + "code": { + TargetName: "code", + }, + }, + "SearchInput": { + "query": { + TargetName: "query", + }, + "limit": { + TargetName: "limit", + }, + }, + "ActionInput": { + "type": { + TargetName: "type", + }, + "payload": { + TargetName: "payload", + }, + }, + "SearchResult": { + "product": { + TargetName: "product", + }, + "user": { + TargetName: "user", + }, + "category": { + TargetName: "category", + }, + }, + "ActionResult": { + "actionSuccess": { + TargetName: "action_success", + }, + "actionError": { + TargetName: "action_error", + }, + }, + }, + } +} From d6713eb5425b2141a1f2745ad0b1636a13c257f7 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 16:00:11 +0200 Subject: [PATCH 11/13] chore: remove duplicate test --- .../grpc_datasource/execution_plan_test.go | 66 ------------------- 1 file changed, 66 deletions(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go index a1b380819a..5c5f066aba 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_test.go @@ -1506,72 +1506,6 @@ func TestCompositeTypeExecutionPlan(t *testing.T) { }, }, }, - { - name: "Should create an execution plan for a query with a union type", - query: "query UnionQuery { randomPet { id name kind ... on Cat { meowVolume } ... on Dog { barkVolume } } }", - expectedPlan: &RPCExecutionPlan{ - Calls: []RPCCall{ - { - ServiceName: "Products", - MethodName: "QueryRandomPet", - Request: RPCMessage{ - Name: "QueryRandomPetRequest", - }, - Response: RPCMessage{ - Name: "QueryRandomPetResponse", - Fields: []RPCField{ - { - Name: "random_pet", - TypeName: string(DataTypeMessage), - JSONPath: "randomPet", - Message: &RPCMessage{ - Name: "Animal", - OneOfType: OneOfTypeInterface, - MemberTypes: []string{ - "Cat", - "Dog", - }, - FieldSelectionSet: RPCFieldSelectionSet{ - "Cat": { - { - Name: "meow_volume", - TypeName: string(DataTypeInt32), - JSONPath: "meowVolume", - }, - }, - "Dog": { - { - Name: "bark_volume", - TypeName: string(DataTypeInt32), - JSONPath: "barkVolume", - }, - }, - }, - Fields: []RPCField{ - { - Name: "id", - TypeName: string(DataTypeString), - JSONPath: "id", - }, - { - Name: "name", - TypeName: string(DataTypeString), - JSONPath: "name", - }, - { - Name: "kind", - TypeName: string(DataTypeString), - JSONPath: "kind", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, { name: "Should create an execution plan for a query with all pets (interface list)", query: "query AllPetsQuery { allPets { id name kind ... on Cat { meowVolume } ... on Dog { barkVolume } } }", From 4033a74a611cadd766b630107f5e2e7284b4fa1f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 3 Jul 2025 16:10:28 +0200 Subject: [PATCH 12/13] chore: remove bitwise field instruction --- v2/pkg/engine/datasource/grpc_datasource/execution_plan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index c95cf879d7..ad67f71efc 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -20,7 +20,7 @@ type OneOfType uint8 // OneOfType constants define the different types of oneof fields. const ( // OneOfTypeNone represents no oneof type (default/zero value) - OneOfTypeNone OneOfType = 1 << iota + OneOfTypeNone OneOfType = iota // OneOfTypeInterface represents an interface type oneof field OneOfTypeInterface // OneOfTypeUnion represents a union type oneof field From 7fd151851eabca233b0593a1303b37319e7ba6e0 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Fri, 4 Jul 2025 10:05:09 +0200 Subject: [PATCH 13/13] chore: use switch --- v2/pkg/engine/datasource/grpc_datasource/execution_plan.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index ad67f71efc..1eb2b6ca56 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -84,7 +84,12 @@ type RPCMessage struct { // IsOneOf checks if the message is a oneof field. func (r *RPCMessage) IsOneOf() bool { - return r.OneOfType > OneOfTypeNone && r.OneOfType <= OneOfTypeUnion + switch r.OneOfType { + case OneOfTypeInterface, OneOfTypeUnion: + return true + } + + return false } // SelectValidTypes returns the valid types for a given type name.