From 0cf6a478bea5b094fc015b2a705d8d3b588a1364 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 20 Jul 2026 12:22:00 +0200 Subject: [PATCH 1/2] fix: incorrect alias behavior for required fields --- .../grpc_datasource/execution_plan.go | 2 +- .../execution_plan_requires_test.go | 426 ++++++++++++++++++ .../execution_plan_visitor_federation.go | 36 ++ .../grpc_datasource_federation_test.go | 96 ++++ 4 files changed, 559 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 2930167763..1be8868242 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -1404,7 +1404,7 @@ func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraph MethodName: rpcConfig.RPC, ResponsePath: ast.Path{ {Kind: ast.FieldName, FieldName: []byte("_entities")}, - {Kind: ast.FieldName, FieldName: []byte(requiredField.fieldName)}, + {Kind: ast.FieldName, FieldName: []byte(requiredField.resultField.AliasOrPath())}, }, Request: RPCMessage{ Name: rpcConfig.Request, diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_requires_test.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_requires_test.go index d70a2c5e9d..c5bd4960d2 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_requires_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_requires_test.go @@ -187,6 +187,432 @@ func TestExecutionPlan_FederationRequires(t *testing.T) { }, }, }, + { + name: "Should create an execution plan for an entity lookup with an aliased required field", + query: `query EntityLookup($representations: [_Any!]!) { _entities(representations: $representations) { ... on Warehouse { __typename name location aliasedScore: stockHealthScore } } }`, + mapping: testMapping(), + federationConfigs: plan.FederationFieldConfigurations{ + { + TypeName: "Warehouse", + SelectionSet: "id", + }, + { + TypeName: "Warehouse", + FieldName: "stockHealthScore", + SelectionSet: "inventoryCount restockData { lastRestockDate }", + }, + }, + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "LookupWarehouseById", + Kind: CallKindEntity, + RequestedEntityType: "Warehouse", + Request: RPCMessage{ + Name: "LookupWarehouseByIdRequest", + Fields: []RPCField{ + { + Name: "keys", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "representations", + Message: &RPCMessage{ + Name: "LookupWarehouseByIdRequestKey", + MemberTypes: []string{"Warehouse"}, + Fields: []RPCField{ + { + Name: "id", + ProtoTypeName: DataTypeString, + JSONPath: "id", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "LookupWarehouseByIdResponse", + Fields: []RPCField{ + { + Name: "result", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "_entities", + Message: &RPCMessage{ + Name: "Warehouse", + Fields: []RPCField{ + { + Name: "__typename", + ProtoTypeName: DataTypeString, + JSONPath: "__typename", + StaticValue: "Warehouse", + }, + { + Name: "name", + ProtoTypeName: DataTypeString, + JSONPath: "name", + }, + { + Name: "location", + ProtoTypeName: DataTypeString, + JSONPath: "location", + }, + }, + }, + }, + }, + }, + }, + { + ID: 1, + ServiceName: "Products", + Kind: CallKindRequired, + MethodName: "RequireWarehouseStockHealthScoreById", + ResponsePath: buildPath("_entities.aliasedScore"), + Request: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdRequest", + Fields: []RPCField{ + { + Name: "context", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "representations", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdContext", + Fields: []RPCField{ + { + Name: "key", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "LookupWarehouseByIdRequestKey", + MemberTypes: []string{"Warehouse"}, + Fields: []RPCField{ + { + Name: "id", + ProtoTypeName: DataTypeString, + JSONPath: "id", + }, + }, + }, + }, + { + Name: "fields", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields", + Fields: []RPCField{ + { + Name: "inventory_count", + ProtoTypeName: DataTypeInt32, + JSONPath: "inventoryCount", + }, + { + Name: "restock_data", + ProtoTypeName: DataTypeMessage, + JSONPath: "restockData", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields.RestockData", + Fields: []RPCField{ + { + Name: "last_restock_date", + ProtoTypeName: DataTypeString, + JSONPath: "lastRestockDate", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResponse", + Fields: []RPCField{ + { + Name: "result", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "result", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResult", + Fields: RPCFields{ + { + Name: "stock_health_score", + ProtoTypeName: DataTypeDouble, + JSONPath: "stockHealthScore", + Alias: "aliasedScore", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should create one require call per response key for plain and aliased required field instances", + query: `query EntityLookup($representations: [_Any!]!) { _entities(representations: $representations) { ... on Warehouse { __typename name location stockHealthScore aliasedScore: stockHealthScore } } }`, + mapping: testMapping(), + federationConfigs: plan.FederationFieldConfigurations{ + { + TypeName: "Warehouse", + SelectionSet: "id", + }, + { + TypeName: "Warehouse", + FieldName: "stockHealthScore", + SelectionSet: "inventoryCount restockData { lastRestockDate }", + }, + }, + expectedPlan: &RPCExecutionPlan{ + Calls: []RPCCall{ + { + ServiceName: "Products", + MethodName: "LookupWarehouseById", + Kind: CallKindEntity, + RequestedEntityType: "Warehouse", + Request: RPCMessage{ + Name: "LookupWarehouseByIdRequest", + Fields: []RPCField{ + { + Name: "keys", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "representations", + Message: &RPCMessage{ + Name: "LookupWarehouseByIdRequestKey", + MemberTypes: []string{"Warehouse"}, + Fields: []RPCField{ + { + Name: "id", + ProtoTypeName: DataTypeString, + JSONPath: "id", + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "LookupWarehouseByIdResponse", + Fields: []RPCField{ + { + Name: "result", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "_entities", + Message: &RPCMessage{ + Name: "Warehouse", + Fields: []RPCField{ + { + Name: "__typename", + ProtoTypeName: DataTypeString, + JSONPath: "__typename", + StaticValue: "Warehouse", + }, + { + Name: "name", + ProtoTypeName: DataTypeString, + JSONPath: "name", + }, + { + Name: "location", + ProtoTypeName: DataTypeString, + JSONPath: "location", + }, + }, + }, + }, + }, + }, + }, + { + ID: 1, + ServiceName: "Products", + Kind: CallKindRequired, + MethodName: "RequireWarehouseStockHealthScoreById", + ResponsePath: buildPath("_entities.stockHealthScore"), + Request: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdRequest", + Fields: []RPCField{ + { + Name: "context", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "representations", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdContext", + Fields: []RPCField{ + { + Name: "key", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "LookupWarehouseByIdRequestKey", + MemberTypes: []string{"Warehouse"}, + Fields: []RPCField{ + { + Name: "id", + ProtoTypeName: DataTypeString, + JSONPath: "id", + }, + }, + }, + }, + { + Name: "fields", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields", + Fields: []RPCField{ + { + Name: "inventory_count", + ProtoTypeName: DataTypeInt32, + JSONPath: "inventoryCount", + }, + { + Name: "restock_data", + ProtoTypeName: DataTypeMessage, + JSONPath: "restockData", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields.RestockData", + Fields: []RPCField{ + { + Name: "last_restock_date", + ProtoTypeName: DataTypeString, + JSONPath: "lastRestockDate", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResponse", + Fields: []RPCField{ + { + Name: "result", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "result", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResult", + Fields: RPCFields{ + { + Name: "stock_health_score", + ProtoTypeName: DataTypeDouble, + JSONPath: "stockHealthScore", + }, + }, + }, + }, + }, + }, + }, + { + ID: 2, + ServiceName: "Products", + Kind: CallKindRequired, + MethodName: "RequireWarehouseStockHealthScoreById", + ResponsePath: buildPath("_entities.aliasedScore"), + Request: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdRequest", + Fields: []RPCField{ + { + Name: "context", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "representations", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdContext", + Fields: []RPCField{ + { + Name: "key", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "LookupWarehouseByIdRequestKey", + MemberTypes: []string{"Warehouse"}, + Fields: []RPCField{ + { + Name: "id", + ProtoTypeName: DataTypeString, + JSONPath: "id", + }, + }, + }, + }, + { + Name: "fields", + ProtoTypeName: DataTypeMessage, + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields", + Fields: []RPCField{ + { + Name: "inventory_count", + ProtoTypeName: DataTypeInt32, + JSONPath: "inventoryCount", + }, + { + Name: "restock_data", + ProtoTypeName: DataTypeMessage, + JSONPath: "restockData", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdFields.RestockData", + Fields: []RPCField{ + { + Name: "last_restock_date", + ProtoTypeName: DataTypeString, + JSONPath: "lastRestockDate", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Response: RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResponse", + Fields: []RPCField{ + { + Name: "result", + ProtoTypeName: DataTypeMessage, + Repeated: true, + JSONPath: "result", + Message: &RPCMessage{ + Name: "RequireWarehouseStockHealthScoreByIdResult", + Fields: RPCFields{ + { + Name: "stock_health_score", + ProtoTypeName: DataTypeDouble, + JSONPath: "stockHealthScore", + Alias: "aliasedScore", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, { name: "Should create an execution plan for tagSummary requiring tags list", query: `query EntityLookup($representations: [_Any!]!) { _entities(representations: $representations) { ... on Storage { __typename name tagSummary } } }`, diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go index 7656f97594..e62d1e502c 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go @@ -437,6 +437,19 @@ func (r *rpcPlanVisitorFederation) enterRequiredField(ref, fieldDefRef int, pare return } + // A required field selected under multiple response keys (e.g. a field and an alias of it) + // needs one entry per key. Reuse the entry already bound to this key, otherwise clone a new + // one; the first instance keeps the configured entry. + if requiredField.ref != ast.InvalidRef && requiredField.resultField.AliasOrPath() != field.AliasOrPath() { + if boundIndex := config.findRequiredFieldByResponseKey(fieldName, field.AliasOrPath()); boundIndex != ast.InvalidRef { + index, requiredField = boundIndex, config.requiredFields[boundIndex] + } else { + index = len(config.requiredFields) + config.requiredFields = append(config.requiredFields, requiredField.clone()) + r.entityConfig.setEntity(r.entityInfo.typeName, config) + } + } + requiredField.ref = ref requiredField.fieldDefRef = fieldDefRef requiredField.resultField = field @@ -628,6 +641,16 @@ type entityConfigData struct { requiredFields []requiredField } +// clone creates a new required field with the same definition but without the result field and field arguments. +func (r requiredField) clone() requiredField { + return requiredField{ + fieldName: r.fieldName, + ref: r.ref, + fieldDefRef: r.fieldDefRef, + selectionSet: r.selectionSet, + } +} + func (e entityConfigData) findRequiredField(fieldName string) (int, requiredField) { for i, rf := range e.requiredFields { if rf.fieldName == fieldName { @@ -638,6 +661,19 @@ func (e entityConfigData) findRequiredField(fieldName string) (int, requiredFiel return ast.InvalidRef, requiredField{} } +// findRequiredFieldByResponseKey returns the index of the requiredField entry for the given +// field name that is already bound to an operation field instance with the given response key +// (alias when present, field name otherwise), or ast.InvalidRef if there is none. +func (e entityConfigData) findRequiredFieldByResponseKey(fieldName, responseKey string) int { + for i, rf := range e.requiredFields { + if rf.fieldName == fieldName && rf.ref != ast.InvalidRef && rf.resultField.AliasOrPath() == responseKey { + return i + } + } + + return ast.InvalidRef +} + func (e entityConfig) setEntity(typeName string, data entityConfigData) { e[typeName] = data } diff --git a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_federation_test.go b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_federation_test.go index 9886fd32b6..89d49c7dca 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_federation_test.go +++ b/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_federation_test.go @@ -731,6 +731,102 @@ func Test_DataSource_Load_WithEntity_Calls_And_Requires(t *testing.T) { require.Empty(t, errorData) }, }, + { + name: "Query Storage type with aliased required field", + query: `query($representations: [_Any!]!) { _entities(representations: $representations) { ...on Storage { id name aliasedScore: stockHealthScore } } }`, + vars: `{"variables":{"representations":[ + {"__typename":"Storage","id":"1","itemCount":100,"restockData":{"lastRestockDate":"2021-01-01"}}, + {"__typename":"Storage","id":"2","itemCount":200,"restockData":{"lastRestockDate":"2021-01-02"}}, + {"__typename":"Storage","id":"3","itemCount":300,"restockData":{"lastRestockDate":"2021-01-03"}}, + {"__typename":"Storage","id":"4","itemCount":400,"restockData":{"lastRestockDate":"2021-01-04"}} + ]}}`, + federationConfigs: plan.FederationFieldConfigurations{ + { + TypeName: "Storage", + SelectionSet: "id", + }, + { + TypeName: "Storage", + FieldName: "stockHealthScore", + SelectionSet: "itemCount restockData { lastRestockDate }", + }, + }, + validate: func(t *testing.T, data map[string]any) { + entities, ok := data["_entities"].([]any) + require.True(t, ok, "_entities should be an array") + require.Len(t, entities, 4, "Should return 4 entities") + + // Storage 1: itemCount=100, restockData provided -> score = 100*0.1 + 10 = 20.0 + storage1, ok := entities[0].(map[string]any) + require.True(t, ok, "storage1 should be an object") + require.Equal(t, "1", storage1["id"]) + require.Equal(t, "Storage 1", storage1["name"]) + require.Equal(t, 20.0, storage1["aliasedScore"]) + + // Storage 2: itemCount=200, restockData provided -> score = 200*0.1 + 10 = 30.0 + storage2, ok := entities[1].(map[string]any) + require.True(t, ok, "storage2 should be an object") + require.Equal(t, "2", storage2["id"]) + require.Equal(t, "Storage 2", storage2["name"]) + require.Equal(t, 30.0, storage2["aliasedScore"]) + + // Storage 3: itemCount=300, restockData provided -> score = 300*0.1 + 10 = 40.0 + storage3, ok := entities[2].(map[string]any) + require.True(t, ok, "storage3 should be an object") + require.Equal(t, "3", storage3["id"]) + require.Equal(t, "Storage 3", storage3["name"]) + require.Equal(t, 40.0, storage3["aliasedScore"]) + + // Storage 4: itemCount=400, restockData provided -> score = 400*0.1 + 10 = 50.0 + storage4, ok := entities[3].(map[string]any) + require.True(t, ok, "storage4 should be an object") + require.Equal(t, "4", storage4["id"]) + require.Equal(t, "Storage 4", storage4["name"]) + require.Equal(t, 50.0, storage4["aliasedScore"]) + }, + validateError: func(t *testing.T, errorData []graphqlError) { + require.Empty(t, errorData) + }, + }, + { + name: "Query Storage type with plain and aliased required field instances", + query: `query($representations: [_Any!]!) { _entities(representations: $representations) { ...on Storage { id name stockHealthScore aliasedScore: stockHealthScore } } }`, + vars: `{"variables":{"representations":[ + {"__typename":"Storage","id":"1","itemCount":100,"restockData":{"lastRestockDate":"2021-01-01"}}, + {"__typename":"Storage","id":"2","itemCount":200,"restockData":{"lastRestockDate":"2021-01-02"}}, + {"__typename":"Storage","id":"3","itemCount":300,"restockData":{"lastRestockDate":"2021-01-03"}}, + {"__typename":"Storage","id":"4","itemCount":400,"restockData":{"lastRestockDate":"2021-01-04"}} + ]}}`, + federationConfigs: plan.FederationFieldConfigurations{ + { + TypeName: "Storage", + SelectionSet: "id", + }, + { + TypeName: "Storage", + FieldName: "stockHealthScore", + SelectionSet: "itemCount restockData { lastRestockDate }", + }, + }, + validate: func(t *testing.T, data map[string]any) { + entities, ok := data["_entities"].([]any) + require.True(t, ok, "_entities should be an array") + require.Len(t, entities, 4, "Should return 4 entities") + + expectedScores := []float64{20.0, 30.0, 40.0, 50.0} + for i, expectedScore := range expectedScores { + storage, ok := entities[i].(map[string]any) + require.True(t, ok, "storage%d should be an object", i+1) + require.Equal(t, fmt.Sprintf("%d", i+1), storage["id"]) + require.Equal(t, fmt.Sprintf("Storage %d", i+1), storage["name"]) + require.Equal(t, expectedScore, storage["stockHealthScore"], "plain response key of storage%d", i+1) + require.Equal(t, expectedScore, storage["aliasedScore"], "aliased response key of storage%d", i+1) + } + }, + validateError: func(t *testing.T, errorData []graphqlError) { + require.Empty(t, errorData) + }, + }, { name: "Query Storage with empty restockData (no +10 bonus)", query: `query($representations: [_Any!]!) { _entities(representations: $representations) { ...on Storage { id name stockHealthScore } } }`, From 6fa75ac3b7b33cc0dfefd74a54e6c3d5d164a08a Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 21 Jul 2026 15:45:13 +0200 Subject: [PATCH 2/2] chore: improve planning for required fields (#1606) This PR adds tests and reformats the logic for planning required fields in connect @coderabbitai summary ## Checklist - [ ] I have discussed my proposed changes in an issue and have received approval to proceed. - [ ] I have followed the coding standards of the project. - [ ] Tests or benchmarks have been added or updated. ## Open Source AI Manifesto This project follows the principles of the [Open Source AI Manifesto](https://human-oss.dev). Please ensure your contribution aligns with its principles. --- .../execution_engine_grpc_requires_test.go | 344 ++++++ .../grpc_datasource/execution_plan.go | 46 +- .../execution_plan_visitor_federation.go | 150 +-- v2/pkg/grpctest/schema.go | 1061 ++++++++++++++--- 4 files changed, 1334 insertions(+), 267 deletions(-) create mode 100644 execution/engine/execution_engine_grpc_requires_test.go diff --git a/execution/engine/execution_engine_grpc_requires_test.go b/execution/engine/execution_engine_grpc_requires_test.go new file mode 100644 index 0000000000..3af6636c94 --- /dev/null +++ b/execution/engine/execution_engine_grpc_requires_test.go @@ -0,0 +1,344 @@ +//go:build !windows + +package engine + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/jensneuse/abstractlogger" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "github.com/wundergraph/graphql-go-tools/execution/graphql" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/graphql_datasource" + grpcdatasource "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/grpc_datasource" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest" + "github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest/mapping" +) + +// requiresSupergraphSDL is the shared supergraph (engine) schema for the @requires tests. It +// describes every field the test operations select. Federation ownership is NOT expressed here — +// it lives in each subgraph's ServiceSDL + DataSourceMetadata. Root fields returning the entities +// (storageProvider/warehouseProvider) are owned by the "owning" subgraph; the @requires fields and +// name/location are owned by the gRPC subgraph. +const requiresSupergraphSDL = ` + type Query { + storageProvider(id: ID!): Storage + warehouseProvider(id: ID!): Warehouse + } + + type Storage { + id: ID! + name: String! + location: String! + itemCount: Int! + restockData: RestockData! + tags: [String!]! + metadata: StorageMetadata! + stockHealthScore: Float! + tagSummary: String! + metadataScore: Float! + filteredTagSummary(prefix: String!): String + } + + type Warehouse { + id: ID! + name: String! + location: String! + inventoryCount: Int! + restockData: RestockData! + stockHealthScore: Float! + } + + type RestockData { + lastRestockDate: String! + } + + type StorageMetadata { + capacity: Int! + zone: String! + priority: Int! + } +` + +// owningSubgraphSDL is the single, shared SDL for the "owning" subgraph across all @requires cases. +// It owns the entity root fields plus every field the gRPC subgraph consumes via @requires (from the +// gRPC perspective those are @external). Individual cases only vary the mocked response, never this. +const owningSubgraphSDL = ` + type Query { + storageProvider(id: ID!): Storage + warehouseProvider(id: ID!): Warehouse + } + + type Storage @key(fields: "id") { + id: ID! + itemCount: Int! + restockData: RestockData! + tags: [String!]! + metadata: StorageMetadata! + } + + type Warehouse @key(fields: "id") { + id: ID! + inventoryCount: Int! + restockData: RestockData! + } + + type RestockData { + lastRestockDate: String! + } + + type StorageMetadata { + capacity: Int! + zone: String! + } +` + +// requiresFieldConfigurations covers the arguments of every field the test operations use: the +// entity root fields and the @requires field that also takes an argument. +var requiresFieldConfigurations = plan.FieldConfigurations{ + { + TypeName: "Query", + FieldName: "storageProvider", + Arguments: []plan.ArgumentConfiguration{{Name: "id", SourceType: plan.FieldArgumentSource}}, + }, + { + TypeName: "Query", + FieldName: "warehouseProvider", + Arguments: []plan.ArgumentConfiguration{{Name: "id", SourceType: plan.FieldArgumentSource}}, + }, + { + TypeName: "Storage", + FieldName: "filteredTagSummary", + Arguments: []plan.ArgumentConfiguration{{Name: "prefix", SourceType: plan.FieldArgumentSource}}, + }, +} + +// newOwningSubgraphMetadata returns a fresh metadata instance describing what the owning subgraph +// owns (a superset covering every @requires input across the cases) plus the entity @keys. A fresh +// instance is returned per call because NewDataSourceConfiguration mutates it via Init(), and the +// subtests run in parallel. +func newOwningSubgraphMetadata() *plan.DataSourceMetadata { + return &plan.DataSourceMetadata{ + RootNodes: []plan.TypeField{ + {TypeName: "Query", FieldNames: []string{"storageProvider", "warehouseProvider"}}, + {TypeName: "Storage", FieldNames: []string{"id", "itemCount", "restockData", "tags", "metadata"}}, + {TypeName: "Warehouse", FieldNames: []string{"id", "inventoryCount", "restockData"}}, + }, + ChildNodes: []plan.TypeField{ + {TypeName: "RestockData", FieldNames: []string{"lastRestockDate"}}, + {TypeName: "StorageMetadata", FieldNames: []string{"capacity", "zone"}}, + }, + FederationMetaData: plan.FederationMetaData{ + Keys: plan.FederationFieldConfigurations{ + {TypeName: "Storage", SelectionSet: "id"}, + {TypeName: "Warehouse", SelectionSet: "id"}, + }, + }, + } +} + +// requiresTestCase is one @requires scenario exercised end-to-end through the engine. Only the +// mocked owning-subgraph response, the operation and the assertion vary; the owning subgraph's SDL +// and metadata are shared across all cases. +type requiresTestCase struct { + name string + // owningResponseJSON is the fixed upstream response the owning subgraph returns; it must contain + // the entity's __typename, key and the fields referenced by the @requires selection set so the + // planner can build the representation for the jump. + owningResponseJSON string + operation string + // assert validates the raw engine response for this case. + assert func(t *testing.T, response string) +} + +// expectJSON asserts the engine response equals the given JSON (order-independent). +func expectJSON(expected string) func(t *testing.T, response string) { + return func(t *testing.T, response string) { + require.JSONEq(t, expected, response) + } +} + +func TestGRPCSubgraphRequiresFullExecution(t *testing.T) { + t.Parallel() + + conn := setupGRPCTestGoPluginServer(t) + + testCases := []requiresTestCase{ + { + // Scalar @requires with a nested selection: itemCount + restockData { lastRestockDate }. + // Also selects name (resolved by the gRPC entity lookup) to cover lookup + requires together. + // stockHealthScore = itemCount*0.1 + 10 (restockData provided) = 100*0.1 + 10 = 20.0. + name: "Storage scalar @requires with nested selection", + owningResponseJSON: `{"data":{"storageProvider":{"__typename":"Storage","id":"1","itemCount":100,"restockData":{"__typename":"RestockData","lastRestockDate":"2021-01-01"}}}}`, + operation: `query { storageProvider(id: "1") { name stockHealthScore } }`, + assert: expectJSON(`{"data":{"storageProvider":{"name":"Storage 1","stockHealthScore":20}}}`), + }, + { + // @requires on a list scalar: tagSummary requires "tags". Mock joins tags with ", ". + name: "Storage @requires a scalar list", + owningResponseJSON: `{"data":{"storageProvider":{"__typename":"Storage","id":"1","tags":["alpha","beta","gamma"]}}}`, + operation: `query { storageProvider(id: "1") { tagSummary } }`, + assert: expectJSON(`{"data":{"storageProvider":{"tagSummary":"alpha, beta, gamma"}}}`), + }, + { + // @requires on nested object fields: metadataScore requires "metadata { capacity zone }". + // Mock: capacity * zoneWeight; zone "A" => 1.0, so 100 * 1.0 = 100.0. + name: "Storage @requires nested object fields", + owningResponseJSON: `{"data":{"storageProvider":{"__typename":"Storage","id":"1","metadata":{"capacity":100,"zone":"A"}}}}`, + operation: `query { storageProvider(id: "1") { metadataScore } }`, + assert: expectJSON(`{"data":{"storageProvider":{"metadataScore":100}}}`), + }, + { + // Same @requires machinery on a different entity (Warehouse.stockHealthScore requires + // "inventoryCount restockData { lastRestockDate }"), which exercises the error path: the + // LookupWarehouseById mock deliberately returns one fewer entity than requested (see + // grpctest/mockservice_lookup.go), so the engine must surface the subgraph entity-count + // error and null the field rather than fabricate data. This still verifies Warehouse's + // @requires config is wired and that the jump is planned for a second entity type. + name: "Warehouse @requires surfaces subgraph entity-count error", + owningResponseJSON: `{"data":{"warehouseProvider":{"__typename":"Warehouse","id":"2","inventoryCount":200,"restockData":{"__typename":"RestockData","lastRestockDate":"2021-01-02"}}}}`, + operation: `query { warehouseProvider(id: "2") { stockHealthScore } }`, + assert: func(t *testing.T, response string) { + require.Contains(t, response, "entity type Warehouse received 0 entities", "response was: %s", response) + require.Contains(t, response, `"warehouseProvider":null`, "response was: %s", response) + }, + }, + { + // @requires combined with a field argument: filteredTagSummary(prefix) requires "tags". + // Mock keeps tags with the given prefix: prefix "ap" over [apple apricot banana] => "apple, apricot". + name: "Storage @requires with a field argument", + owningResponseJSON: `{"data":{"storageProvider":{"__typename":"Storage","id":"1","tags":["apple","apricot","banana"]}}}`, + operation: `query { storageProvider(id: "1") { filteredTagSummary(prefix: "ap") } }`, + assert: expectJSON(`{"data":{"storageProvider":{"filteredTagSummary":"apple, apricot"}}}`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Both subgraph setups live side by side: the owning subgraph provides the entity key and + // the @requires inputs, the gRPC subgraph resolves the @requires field. + owningDS := setupOwningSubgraph(t, tc.owningResponseJSON) + grpcDS := setupGRPCProductsSubgraph(t, conn) + + response := runRequiresOperation(t, []plan.DataSource{owningDS, grpcDS}, tc.operation) + + tc.assert(t, response) + }) + } +} + +// setupOwningSubgraph builds the "owning" subgraph: a graphql_datasource over an httptest.Server +// that returns responseJSON for any request. Its SDL (owningSubgraphSDL) and metadata are shared +// across all cases; only responseJSON varies. It owns the entity root fields plus the fields the +// gRPC subgraph consumes via @requires. +func setupOwningSubgraph(t *testing.T, responseJSON string) plan.DataSource { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(responseJSON)) + })) + t.Cleanup(server.Close) + + config, err := graphql_datasource.NewConfiguration(graphql_datasource.ConfigurationInput{ + Fetch: &graphql_datasource.FetchConfiguration{URL: server.URL}, + SchemaConfiguration: mustSchemaConfig(t, + &graphql_datasource.FederationConfiguration{Enabled: true, ServiceSDL: owningSubgraphSDL}, + owningSubgraphSDL, + ), + }) + require.NoError(t, err) + + ds, err := plan.NewDataSourceConfiguration[graphql_datasource.Configuration]( + "owning-subgraph", + mustFactory(t, http.DefaultClient), + newOwningSubgraphMetadata(), + config, + ) + require.NoError(t, err) + + return ds +} + +// setupGRPCProductsSubgraph builds the gRPC subgraph over the go-plugin harness. It reuses the +// shared grpctest datasource metadata (the full products metadata, incl. every entity's @key and +// @requires config) and mapping; fields/types absent from a given test's operation are simply never +// planned, so advertising the extra nodes is harmless. Its SchemaConfiguration uses the products SDL +// (with @key/@external/@requires) so the proto compiler maps operations correctly. This subgraph +// owns name/location and resolves the @requires fields; the entity keys' external inputs are owned +// by the owning subgraph. +func setupGRPCProductsSubgraph(t *testing.T, conn grpc.ClientConnInterface) plan.DataSource { + t.Helper() + + grpcMapping := mapping.MustDefaultGRPCMapping(t) + + factory, err := graphql_datasource.NewFactoryGRPC(context.Background(), conn) + require.NoError(t, err) + + protoSchema, err := grpctest.ProtoSchema() + require.NoError(t, err) + + compiler, err := grpcdatasource.NewProtoCompiler(protoSchema, grpcMapping) + require.NoError(t, err) + + grpcSchemaDoc, err := grpctest.GraphQLSchemaWithoutBaseDefinitions() + require.NoError(t, err) + subgraphSDL := string(grpcSchemaDoc.Input.RawBytes) + + config, err := graphql_datasource.NewConfiguration(graphql_datasource.ConfigurationInput{ + GRPC: &grpcdatasource.GRPCConfiguration{Mapping: grpcMapping, Compiler: compiler}, + SchemaConfiguration: mustSchemaConfig(t, + &graphql_datasource.FederationConfiguration{Enabled: true, ServiceSDL: subgraphSDL}, + subgraphSDL, + ), + }) + require.NoError(t, err) + + ds, err := plan.NewDataSourceConfiguration[graphql_datasource.Configuration]( + "grpc-subgraph", + factory, + grpctest.GetDataSourceMetadata(), + config, + ) + require.NoError(t, err) + + return ds +} + +// runRequiresOperation builds an engine over the given data sources and the shared supergraph +// schema, executes the operation and returns the raw JSON response. +func runRequiresOperation(t *testing.T, dataSources []plan.DataSource, operation string) string { + t.Helper() + + inputSchema, err := graphql.NewSchemaFromString(requiresSupergraphSDL) + require.NoError(t, err) + + engineConf := NewConfiguration(inputSchema) + engineConf.SetDataSources(dataSources) + engineConf.SetFieldConfigurations(requiresFieldConfigurations) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + engine, err := NewExecutionEngine(ctx, abstractlogger.Noop{}, engineConf, resolve.ResolverOptions{ + MaxConcurrency: 1024, + PropagateSubgraphErrors: true, + SubgraphErrorPropagationMode: resolve.SubgraphErrorPropagationModeWrapped, + }) + require.NoError(t, err) + + request := graphql.Request{Query: operation} + + resultWriter := graphql.NewEngineResultWriter() + require.NoError(t, engine.Execute(ctx, &request, &resultWriter)) + + return resultWriter.String() +} diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go index 1be8868242..5dbae80ebd 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan.go @@ -1340,8 +1340,8 @@ func (r *rpcPlanningContext) fieldDefinitionRefForType(fieldName, typeName strin } -// createRequiredFieldsRPCCalls creates a new call for each required field. -// It returns a list of calls which are needed to provide certain fields for the entity, which require data from the representation variables. +// createRequiredFieldsRPCCall creates the call needed to provide a required field for an entity, +// which requires data from the representation variables. It produces messages of the form: /* message RequireWarehouseStockHealthScoreByIdRequest { // RequireWarehouseStockHealthScoreByIdContext provides the context for the required fields method RequireWarehouseStockHealthScoreById. @@ -1366,37 +1366,17 @@ message RequireWarehouseStockHealthScoreByIdFields { RestockData restock_data = 2; } */ -func (r *rpcPlanningContext) createRequiredFieldsRPCCalls(callIndex *int, subgraphName string, entityTypeName string, data entityConfigData) ([]RPCCall, error) { - calls := make([]RPCCall, 0, len(data.requiredFields)) - for _, requiredField := range data.requiredFields { - call, err := r.createRequiredFieldsRPCCall(*callIndex, subgraphName, entityTypeName, &requiredField, data) - if err != nil { - return nil, err - } - - *callIndex++ - calls = append(calls, call) - } - - return calls, nil -} - -// createRequiredFieldsRPCCall creates a new required fields RPC call for a given configuration. -func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraphName, typeName string, requiredField *requiredField, data entityConfigData) (RPCCall, error) { - rpcConfig, exists := r.mapping.FindRequiredFieldsRPCConfig(typeName, data.keyFields, requiredField.fieldName) +func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraphName string, rf plannedRequiredField, keyFields string, keyFieldMessage *RPCMessage) (RPCCall, error) { + typeName := rf.typeName + rpcConfig, exists := r.mapping.FindRequiredFieldsRPCConfig(typeName, keyFields, rf.fieldName) if !exists { - return RPCCall{}, fmt.Errorf("required fields RPC config not found for type: %s, field: %s", typeName, requiredField.fieldName) + return RPCCall{}, fmt.Errorf("required fields RPC config not found for type: %s, field: %s", typeName, rf.fieldName) } fieldMessage := &RPCMessage{ Name: rpcConfig.RPC + "Fields", } - fieldDefRef := r.fieldDefinitionRefForType(requiredField.fieldName, typeName) - if fieldDefRef == ast.InvalidRef { - return RPCCall{}, fmt.Errorf("unable to build required field: field definition not found for field %s", requiredField.fieldName) - } - call := RPCCall{ ID: callIndex, ServiceName: r.resolveServiceName(subgraphName), @@ -1404,7 +1384,7 @@ func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraph MethodName: rpcConfig.RPC, ResponsePath: ast.Path{ {Kind: ast.FieldName, FieldName: []byte("_entities")}, - {Kind: ast.FieldName, FieldName: []byte(requiredField.resultField.AliasOrPath())}, + {Kind: ast.FieldName, FieldName: []byte(rf.resultField.AliasOrPath())}, }, Request: RPCMessage{ Name: rpcConfig.Request, @@ -1420,7 +1400,7 @@ func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraph { Name: "key", ProtoTypeName: DataTypeMessage, - Message: data.keyFieldMessage, + Message: keyFieldMessage, }, { Name: requiresArgumentsFieldName, @@ -1442,7 +1422,7 @@ func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraph JSONPath: resultFieldName, Message: &RPCMessage{ Name: rpcConfig.RPC + "Result", - Fields: RPCFields{requiredField.resultField}, + Fields: RPCFields{rf.resultField}, }, }, }, @@ -1453,18 +1433,18 @@ func (r *rpcPlanningContext) createRequiredFieldsRPCCall(callIndex int, subgraph defer walker.Release() vis := newRequiredFieldsVisitor(walker, fieldMessage, r.mapping) - if err := vis.visit(r.definition, typeName, requiredField.selectionSet, requiredFieldVisitorConfig{ + if err := vis.visit(r.definition, typeName, rf.selectionSet, requiredFieldVisitorConfig{ referenceNestedMessages: true, }); err != nil { return RPCCall{}, err } - if len(requiredField.fieldArguments) > 0 { + if len(rf.fieldArguments) > 0 { fieldArgsMessage := &RPCMessage{ Name: rpcConfig.RPC + "Args", } - fieldArgsMessage.Fields = make(RPCFields, len(requiredField.fieldArguments)) - for i, arg := range requiredField.fieldArguments { + fieldArgsMessage.Fields = make(RPCFields, len(rf.fieldArguments)) + for i, arg := range rf.fieldArguments { field, err := r.createRPCFieldFromFieldArgument(arg) if err != nil { return RPCCall{}, err diff --git a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go index e62d1e502c..e30fd712ab 100644 --- a/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go +++ b/v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go @@ -35,6 +35,19 @@ type rpcPlanVisitorFederation struct { fieldResolverAncestors stack[int] resolverFields []resolverField + // plannedRequiredFields accumulates one entry per required-field instance observed + // during the walk. + plannedRequiredFields []plannedRequiredField + + // keyFieldMessages holds the key message built per entity type in scaffoldEntityLookup. + // Both are consumed in LeaveDocument to build the RPC calls. + // + // Keying by type name alone (here and for entityConfig.keyFields) assumes a single fetch + // never looks up the same entity type under two different @key selections. The planner + // guarantees this: it uses one key per type per fetch and splits conflicting keys into + // separate operations. If that ever changes, these lookups must key on (type, key) instead. + keyFieldMessages map[string]*RPCMessage + fieldPath ast.Path } @@ -50,6 +63,7 @@ func newRPCPlanVisitorFederation(config rpcPlanVisitorConfig) *rpcPlanVisitorFed entityInlineFragmentRef: ast.InvalidRef, }, entityConfig: parseFederationConfigData(config.federationConfigs), + keyFieldMessages: make(map[string]*RPCMessage), resolverFields: make([]resolverField, 0), fieldResolverAncestors: newStack[int](0), fieldPath: ast.Path{}.WithFieldNameItem([]byte("result")), @@ -95,21 +109,19 @@ func (r *rpcPlanVisitorFederation) LeaveDocument(_, _ *ast.Document) { r.resolverFields = nil } - for entityTypeName, entityConfigData := range r.entityConfig { - if len(entityConfigData.requiredFields) == 0 { - continue - } - - calls, err = r.planCtx.createRequiredFieldsRPCCalls(&r.callIndex, r.subgraphName, entityTypeName, entityConfigData) + for _, prf := range r.plannedRequiredFields { + call, err := r.planCtx.createRequiredFieldsRPCCall( + r.callIndex, r.subgraphName, prf, + r.entityConfig[prf.typeName].keyFields, + r.keyFieldMessages[prf.typeName], + ) if err != nil { r.walker.StopWithInternalErr(err) return } - if len(calls) > 0 { - r.plan.Calls = append(r.plan.Calls, calls...) - } - + r.callIndex++ + r.plan.Calls = append(r.plan.Calls, call) } } @@ -425,35 +437,21 @@ func (r *rpcPlanVisitorFederation) enterRequiredField(ref, fieldDefRef int, pare field.Message = message } - config, exists := r.entityConfig.getEntity(r.entityInfo.typeName) - if !exists { - r.walker.StopWithInternalErr(fmt.Errorf("entity config not found for type %s", r.entityInfo.typeName)) - return - } - - index, requiredField := config.findRequiredField(fieldName) - if index == ast.InvalidRef { + // Each operation field instance appends its own entry, so a required field selected under + // multiple response keys (e.g. a field and an alias of it) naturally gets one entry per key. + selectionSet, ok := r.entityConfig.requiredFieldSelectionSet(r.entityInfo.typeName, fieldName) + if !ok { r.walker.StopWithInternalErr(fmt.Errorf("required field not found for type %s and field %s", r.entityInfo.typeName, fieldName)) return } - // A required field selected under multiple response keys (e.g. a field and an alias of it) - // needs one entry per key. Reuse the entry already bound to this key, otherwise clone a new - // one; the first instance keeps the configured entry. - if requiredField.ref != ast.InvalidRef && requiredField.resultField.AliasOrPath() != field.AliasOrPath() { - if boundIndex := config.findRequiredFieldByResponseKey(fieldName, field.AliasOrPath()); boundIndex != ast.InvalidRef { - index, requiredField = boundIndex, config.requiredFields[boundIndex] - } else { - index = len(config.requiredFields) - config.requiredFields = append(config.requiredFields, requiredField.clone()) - r.entityConfig.setEntity(r.entityInfo.typeName, config) - } + planned := plannedRequiredField{ + typeName: r.entityInfo.typeName, + fieldName: fieldName, + selectionSet: selectionSet, + resultField: field, } - requiredField.ref = ref - requiredField.fieldDefRef = fieldDefRef - requiredField.resultField = field - fieldArgs := r.operation.FieldArguments(ref) if len(fieldArgs) > 0 { fieldArguments, err := r.planCtx.parseFieldArguments(r.walker, fieldDefRef, fieldArgs) @@ -461,10 +459,10 @@ func (r *rpcPlanVisitorFederation) enterRequiredField(ref, fieldDefRef int, pare r.walker.StopWithInternalErr(err) return } - requiredField.fieldArguments = fieldArguments + planned.fieldArguments = fieldArguments } - config.requiredFields[index] = requiredField + r.plannedRequiredFields = append(r.plannedRequiredFields, planned) } // enterFieldResolver enters a field resolver. @@ -567,7 +565,7 @@ func (r *rpcPlanVisitorFederation) scaffoldEntityLookup(typeName string, ecd ent }, } - r.entityConfig.setEntityKeyMessage(typeName, keyFieldMessage) + r.keyFieldMessages[typeName] = keyFieldMessage entityMessage := &RPCMessage{ Name: typeName, @@ -625,67 +623,34 @@ type entityInfo struct { entityInlineFragmentRef int } +// entityConfig is a read-only lookup built once from the federation field configurations. type entityConfig map[string]entityConfigData -type requiredField struct { - fieldName string - ref int - fieldDefRef int - selectionSet string - resultField RPCField - fieldArguments []fieldArgument -} type entityConfigData struct { - keyFields string - keyFieldMessage *RPCMessage - requiredFields []requiredField + keyFields string + requiredFields map[string]string // fieldName -> @requires selection set } -// clone creates a new required field with the same definition but without the result field and field arguments. -func (r requiredField) clone() requiredField { - return requiredField{ - fieldName: r.fieldName, - ref: r.ref, - fieldDefRef: r.fieldDefRef, - selectionSet: r.selectionSet, - } -} - -func (e entityConfigData) findRequiredField(fieldName string) (int, requiredField) { - for i, rf := range e.requiredFields { - if rf.fieldName == fieldName { - return i, rf - } - } - - return ast.InvalidRef, requiredField{} -} - -// findRequiredFieldByResponseKey returns the index of the requiredField entry for the given -// field name that is already bound to an operation field instance with the given response key -// (alias when present, field name otherwise), or ast.InvalidRef if there is none. -func (e entityConfigData) findRequiredFieldByResponseKey(fieldName, responseKey string) int { - for i, rf := range e.requiredFields { - if rf.fieldName == fieldName && rf.ref != ast.InvalidRef && rf.resultField.AliasOrPath() == responseKey { - return i - } - } - - return ast.InvalidRef -} - -func (e entityConfig) setEntity(typeName string, data entityConfigData) { - e[typeName] = data +// plannedRequiredField is one observed instance of a required field in the operation. One entry +// is appended per operation field instance, so distinct response keys each get their own entry. +type plannedRequiredField struct { + typeName string + fieldName string + selectionSet string // @requires selection set, copied from the federation config + resultField RPCField + fieldArguments []fieldArgument } -func (e entityConfig) setEntityKeyMessage(typeName string, message *RPCMessage) { +// requiredFieldSelectionSet returns the @requires selection set for a required field, +// or ("", false) if the type/field is not a configured required field. +func (e entityConfig) requiredFieldSelectionSet(typeName, fieldName string) (string, bool) { data, ok := e[typeName] if !ok { - return + return "", false } - data.keyFieldMessage = message - e[typeName] = data + sel, ok := data.requiredFields[fieldName] + return sel, ok } func (e entityConfig) getEntity(typeName string) (entityConfigData, bool) { @@ -697,25 +662,20 @@ func parseFederationConfigData(federationConfigs plan.FederationFieldConfigurati config := make(entityConfig) for _, fc := range federationConfigs { - data, ok := config.getEntity(fc.TypeName) + data, ok := config[fc.TypeName] if !ok { data = entityConfigData{ - requiredFields: make([]requiredField, 0), + requiredFields: make(map[string]string), } } if fc.FieldName != "" { - data.requiredFields = append(data.requiredFields, requiredField{ - fieldName: fc.FieldName, - ref: ast.InvalidRef, - fieldDefRef: ast.InvalidRef, - selectionSet: fc.SelectionSet, - }) + data.requiredFields[fc.FieldName] = fc.SelectionSet } else { data.keyFields = fc.SelectionSet } - config.setEntity(fc.TypeName, data) + config[fc.TypeName] = data } return config diff --git a/v2/pkg/grpctest/schema.go b/v2/pkg/grpctest/schema.go index 38d256d42a..7e6be659cd 100644 --- a/v2/pkg/grpctest/schema.go +++ b/v2/pkg/grpctest/schema.go @@ -149,50 +149,50 @@ func GetFieldConfigurations() plan.FieldConfigurations { }, { TypeName: "Query", - FieldName: "categoriesByKind", + FieldName: "calculateTotals", Arguments: []plan.ArgumentConfiguration{ { - Name: "kind", + Name: "orders", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "categoriesByKinds", + FieldName: "category", Arguments: []plan.ArgumentConfiguration{ { - Name: "kinds", + Name: "id", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "filterCategories", + FieldName: "categoriesByKind", Arguments: []plan.ArgumentConfiguration{ { - Name: "filter", + Name: "kind", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "calculateTotals", + FieldName: "categoriesByKinds", Arguments: []plan.ArgumentConfiguration{ { - Name: "orders", + Name: "kinds", SourceType: plan.FieldArgumentSource, }, }, }, { - TypeName: "Mutation", - FieldName: "createUser", + TypeName: "Query", + FieldName: "filterCategories", Arguments: []plan.ArgumentConfiguration{ { - Name: "input", + Name: "filter", SourceType: plan.FieldArgumentSource, }, }, @@ -228,62 +228,68 @@ func GetFieldConfigurations() plan.FieldConfigurations { }, }, { - TypeName: "Mutation", - FieldName: "performAction", + TypeName: "Query", + FieldName: "blogPostById", Arguments: []plan.ArgumentConfiguration{ { - Name: "input", + Name: "id", SourceType: plan.FieldArgumentSource, }, }, }, { - TypeName: "Mutation", - FieldName: "createNullableFieldsType", + TypeName: "Query", + FieldName: "blogPostsWithFilter", Arguments: []plan.ArgumentConfiguration{ { - Name: "input", + Name: "filter", SourceType: plan.FieldArgumentSource, }, }, }, { - TypeName: "Mutation", - FieldName: "updateNullableFieldsType", + TypeName: "Query", + FieldName: "authorById", Arguments: []plan.ArgumentConfiguration{ { Name: "id", SourceType: plan.FieldArgumentSource, }, + }, + }, + { + TypeName: "Query", + FieldName: "authorsWithFilter", + Arguments: []plan.ArgumentConfiguration{ { - Name: "input", + Name: "filter", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "blogPostById", + FieldName: "bulkSearchAuthors", Arguments: []plan.ArgumentConfiguration{ { - Name: "id", + Name: "filters", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "blogPostsWithFilter", + FieldName: "bulkSearchBlogPosts", Arguments: []plan.ArgumentConfiguration{ { - Name: "filter", + Name: "filters", SourceType: plan.FieldArgumentSource, }, }, }, { TypeName: "Query", - FieldName: "authorById", + FieldName: "testContainer", Arguments: []plan.ArgumentConfiguration{ { Name: "id", @@ -293,10 +299,54 @@ func GetFieldConfigurations() plan.FieldConfigurations { }, { TypeName: "Query", - FieldName: "authorsWithFilter", + FieldName: "conditionalSearch", Arguments: []plan.ArgumentConfiguration{ { - Name: "filter", + Name: "conditions", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "createUser", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "input", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "performAction", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "input", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "createNullableFieldsType", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "input", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "updateNullableFieldsType", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "id", + SourceType: plan.FieldArgumentSource, + }, + { + Name: "input", SourceType: plan.FieldArgumentSource, }, }, @@ -350,11 +400,275 @@ func GetFieldConfigurations() plan.FieldConfigurations { }, }, { - TypeName: "Query", - FieldName: "conditionalSearch", + TypeName: "Mutation", + FieldName: "bulkCreateAuthors", Arguments: []plan.ArgumentConfiguration{ { - Name: "conditions", + Name: "authors", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "bulkUpdateAuthors", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "authors", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "bulkCreateBlogPosts", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "blogPosts", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Mutation", + FieldName: "bulkUpdateBlogPosts", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "blogPosts", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Product", + FieldName: "shippingEstimate", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "input", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Product", + FieldName: "recommendedCategory", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "maxPrice", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Product", + FieldName: "mascotRecommendation", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "includeDetails", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Product", + FieldName: "stockStatus", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "checkAvailability", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Product", + FieldName: "productDetails", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "includeExtended", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "storageStatus", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "checkHealth", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "linkedStorages", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "depth", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "nearbyStorages", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "radius", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "filteredTagSummary", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "prefix", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "multiFilteredTagSummary", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "prefixes", + SourceType: plan.FieldArgumentSource, + }, + { + Name: "maxResults", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Storage", + FieldName: "nullableFilteredTagSummary", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "prefix", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "productCount", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "filters", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "popularityScore", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "threshold", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "categoryMetrics", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "metricType", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "mascot", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "includeVolume", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "categoryStatus", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "checkHealth", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "childCategories", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "include", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Category", + FieldName: "optionalCategories", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "include", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Subcategory", + FieldName: "itemCount", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "filters", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "Subcategory", + FieldName: "featuredCategory", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "includeChildren", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "CategoryMetrics", + FieldName: "normalizedScore", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "baseline", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "CategoryMetrics", + FieldName: "relatedCategory", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "include", + SourceType: plan.FieldArgumentSource, + }, + }, + }, + { + TypeName: "TestContainer", + FieldName: "details", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "includeExtended", SourceType: plan.FieldArgumentSource, }, }, @@ -364,13 +678,197 @@ func GetFieldConfigurations() plan.FieldConfigurations { func GetDataSourceMetadata() *plan.DataSourceMetadata { return &plan.DataSourceMetadata{ + FederationMetaData: plan.FederationMetaData{ + Keys: plan.FederationFieldConfigurations{ + { + TypeName: "Product", + SelectionSet: "id", + }, + { + TypeName: "Storage", + SelectionSet: "id", + }, + { + TypeName: "Warehouse", + SelectionSet: "id", + }, + }, + Requires: plan.FederationFieldConfigurations{ + { + TypeName: "Storage", + FieldName: "stockHealthScore", + SelectionSet: "itemCount restockData { lastRestockDate }", + }, + { + TypeName: "Storage", + FieldName: "tagSummary", + SelectionSet: "tags", + }, + { + TypeName: "Storage", + FieldName: "optionalTagSummary", + SelectionSet: "optionalTags", + }, + { + TypeName: "Storage", + FieldName: "metadataScore", + SelectionSet: "metadata { capacity zone }", + }, + { + TypeName: "Storage", + FieldName: "processedMetadata", + SelectionSet: "metadata { capacity zone priority }", + }, + { + TypeName: "Storage", + FieldName: "optionalProcessedMetadata", + SelectionSet: "metadata { capacity zone }", + }, + { + TypeName: "Storage", + FieldName: "processedTags", + SelectionSet: "tags", + }, + { + TypeName: "Storage", + FieldName: "optionalProcessedTags", + SelectionSet: "optionalTags", + }, + { + TypeName: "Storage", + FieldName: "processedMetadataHistory", + SelectionSet: "metadataHistory { capacity zone }", + }, + { + TypeName: "Storage", + FieldName: "kindSummary", + SelectionSet: "storageKind", + }, + { + TypeName: "Storage", + FieldName: "categoryInfoSummary", + SelectionSet: "categoryInfo { kind name }", + }, + { + TypeName: "Storage", + FieldName: "itemInfo", + SelectionSet: "primaryItem { ... on PalletItem { __typename name palletCount } ... on ContainerItem { __typename name containerSize } }", + }, + { + TypeName: "Storage", + FieldName: "operationReport", + SelectionSet: "lastStorageOperation { ... on StorageSuccess { __typename message completedAt } ... on StorageFailure { __typename message errorCode } }", + }, + { + TypeName: "Storage", + FieldName: "securitySummary", + SelectionSet: "securitySetup { securityLevel primaryItem { ... on PalletItem { __typename name palletCount } ... on ContainerItem { __typename name containerSize } } }", + }, + { + TypeName: "Storage", + FieldName: "itemHandlerInfo", + SelectionSet: "primaryItem { ... on PalletItem { __typename handler { name } } ... on ContainerItem { __typename handler { name } } }", + }, + { + TypeName: "Storage", + FieldName: "itemSpecsInfo", + SelectionSet: "primaryItem { ... on PalletItem { __typename specs { name dimensions { length width } } } ... on ContainerItem { __typename specs { name dimensions { length width } } } }", + }, + { + TypeName: "Storage", + FieldName: "deepItemInfo", + SelectionSet: "primaryItem { ... on PalletItem { __typename handler { assignedItem { ... on ContainerItem { __typename name containerSize } ... on PalletItem { __typename name palletCount } } } } ... on ContainerItem { __typename handler { name } } }", + }, + { + TypeName: "Storage", + FieldName: "filteredTagSummary", + SelectionSet: "tags", + }, + { + TypeName: "Storage", + FieldName: "multiFilteredTagSummary", + SelectionSet: "tags", + }, + { + TypeName: "Storage", + FieldName: "nullableFilteredTagSummary", + SelectionSet: "tags", + }, + { + TypeName: "Warehouse", + FieldName: "stockHealthScore", + SelectionSet: "inventoryCount restockData { lastRestockDate }", + }, + }, + }, RootNodes: plan.TypeFields{ + { + TypeName: "Query", + FieldNames: []string{ + "users", + "user", + "nestedType", + "recursiveType", + "typeFilterWithArguments", + "typeWithMultipleFilterFields", + "complexFilterType", + "calculateTotals", + "categories", + "category", + "categoriesByKind", + "categoriesByKinds", + "filterCategories", + "randomPet", + "allPets", + "search", + "randomSearchResult", + "nullableFieldsType", + "nullableFieldsTypeById", + "nullableFieldsTypeWithFilter", + "allNullableFieldsTypes", + "blogPost", + "blogPostById", + "blogPostsWithFilter", + "allBlogPosts", + "author", + "authorById", + "authorsWithFilter", + "allAuthors", + "bulkSearchAuthors", + "bulkSearchBlogPosts", + "testContainer", + "testContainers", + "conditionalSearch", + }, + }, + { + TypeName: "Mutation", + FieldNames: []string{ + "createUser", + "performAction", + "createNullableFieldsType", + "updateNullableFieldsType", + "createBlogPost", + "updateBlogPost", + "createAuthor", + "updateAuthor", + "bulkCreateAuthors", + "bulkUpdateAuthors", + "bulkCreateBlogPosts", + "bulkUpdateBlogPosts", + }, + }, { TypeName: "Product", FieldNames: []string{ "id", "name", "price", + "shippingEstimate", + "recommendedCategory", + "mascotRecommendation", + "stockStatus", + "productDetails", }, }, { @@ -379,6 +877,42 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "id", "name", "location", + "stockHealthScore", + "tagSummary", + "optionalTagSummary", + "metadataScore", + "processedMetadata", + "optionalProcessedMetadata", + "processedTags", + "optionalProcessedTags", + "processedMetadataHistory", + "kindSummary", + "categoryInfoSummary", + "itemInfo", + "operationReport", + "securitySummary", + "itemHandlerInfo", + "itemSpecsInfo", + "deepItemInfo", + "storageStatus", + "linkedStorages", + "nearbyStorages", + "filteredTagSummary", + "multiFilteredTagSummary", + "nullableFilteredTagSummary", + }, + ExternalFieldNames: []string{ + "itemCount", + "restockData", + "tags", + "optionalTags", + "metadata", + "metadataHistory", + "storageKind", + "categoryInfo", + "primaryItem", + "lastStorageOperation", + "securitySetup", }, }, { @@ -387,57 +921,115 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "id", "name", "location", + "stockHealthScore", + }, + ExternalFieldNames: []string{ + "inventoryCount", + "restockData", }, }, + }, + ChildNodes: plan.TypeFields{ { - TypeName: "Query", + TypeName: "Product", + FieldNames: []string{ + "id", + "name", + "price", + "shippingEstimate", + "recommendedCategory", + "mascotRecommendation", + "stockStatus", + "productDetails", + }, + }, + { + TypeName: "ProductDetails", + FieldNames: []string{ + "id", + "description", + "reviewSummary", + "recommendedPet", + }, + }, + { + TypeName: "Storage", + FieldNames: []string{ + "id", + "name", + "location", + "stockHealthScore", + "tagSummary", + "optionalTagSummary", + "metadataScore", + "processedMetadata", + "optionalProcessedMetadata", + "processedTags", + "optionalProcessedTags", + "processedMetadataHistory", + "kindSummary", + "categoryInfoSummary", + "itemInfo", + "operationReport", + "securitySummary", + "itemHandlerInfo", + "itemSpecsInfo", + "deepItemInfo", + "storageStatus", + "linkedStorages", + "nearbyStorages", + "filteredTagSummary", + "multiFilteredTagSummary", + "nullableFilteredTagSummary", + }, + ExternalFieldNames: []string{ + "itemCount", + "restockData", + "tags", + "optionalTags", + "metadata", + "metadataHistory", + "storageKind", + "categoryInfo", + "primaryItem", + "lastStorageOperation", + "securitySetup", + }, + }, + { + TypeName: "Warehouse", + FieldNames: []string{ + "id", + "name", + "location", + "stockHealthScore", + }, + ExternalFieldNames: []string{ + "inventoryCount", + "restockData", + }, + }, + { + TypeName: "RestockData", + FieldNames: []string{ + "lastRestockDate", + }, + }, + { + TypeName: "StorageMetadata", FieldNames: []string{ - "users", - "user", - "nestedType", - "recursiveType", - "typeFilterWithArguments", - "typeWithMultipleFilterFields", - "complexFilterType", - "categories", - "categoriesByKind", - "categoriesByKinds", - "filterCategories", - "randomPet", - "allPets", - "search", - "calculateTotals", - "randomSearchResult", - "nullableFieldsType", - "nullableFieldsTypeById", - "nullableFieldsTypeWithFilter", - "allNullableFieldsTypes", - "blogPost", - "blogPostById", - "blogPostsWithFilter", - "allBlogPosts", - "author", - "authorById", - "authorsWithFilter", - "allAuthors", - "conditionalSearch", + "capacity", + "zone", + "priority", }, }, { - TypeName: "Mutation", + TypeName: "StorageCategoryInfo", FieldNames: []string{ - "createUser", - "performAction", - "createNullableFieldsType", - "updateNullableFieldsType", - "createBlogPost", - "updateBlogPost", - "createAuthor", - "updateAuthor", + "kind", + "name", }, }, - }, - ChildNodes: plan.TypeFields{ { TypeName: "User", FieldNames: []string{ @@ -485,6 +1077,13 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "filterField2", }, }, + { + TypeName: "FilterTypeInput", + FieldNames: []string{ + "filterField1", + "filterField2", + }, + }, { TypeName: "TypeWithComplexFilterInput", FieldNames: []string{ @@ -493,11 +1092,58 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { }, }, { - TypeName: "Category", + TypeName: "FilterType", FieldNames: []string{ - "id", "name", - "kind", + "filterField1", + "filterField2", + "pagination", + }, + }, + { + TypeName: "Pagination", + FieldNames: []string{ + "page", + "perPage", + }, + }, + { + TypeName: "ComplexFilterTypeInput", + FieldNames: []string{ + "filter", + }, + }, + { + TypeName: "OrderLineInput", + FieldNames: []string{ + "productId", + "quantity", + "modifiers", + }, + }, + { + TypeName: "OrderInput", + FieldNames: []string{ + "orderId", + "customerName", + "lines", + }, + }, + { + TypeName: "Order", + FieldNames: []string{ + "orderId", + "customerName", + "totalItems", + "orderLines", + }, + }, + { + TypeName: "OrderLine", + FieldNames: []string{ + "productId", + "quantity", + "modifiers", }, }, { @@ -507,6 +1153,51 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "pagination", }, }, + { + TypeName: "Category", + FieldNames: []string{ + "id", + "name", + "kind", + "productCount", + "subcategories", + "popularityScore", + "categoryMetrics", + "mascot", + "categoryStatus", + "childCategories", + "optionalCategories", + "nullMetrics", + "totalProducts", + "topSubcategory", + "activeSubcategories", + }, + }, + { + TypeName: "Subcategory", + FieldNames: []string{ + "id", + "name", + "description", + "isActive", + "itemCount", + "featuredCategory", + "parentCategory", + }, + }, + { + TypeName: "CategoryMetrics", + FieldNames: []string{ + "id", + "metricType", + "value", + "timestamp", + "categoryId", + "normalizedScore", + "relatedCategory", + "averageScore", + }, + }, { TypeName: "CategoryKind", FieldNames: []string{ @@ -531,6 +1222,8 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "name", "kind", "meowVolume", + "owner", + "breed", }, }, { @@ -540,104 +1233,143 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "name", "kind", "barkVolume", + "owner", + "breed", }, }, { - TypeName: "UserInput", + TypeName: "Owner", FieldNames: []string{ + "id", "name", + "contact", + "pet", }, }, { - TypeName: "Order", + TypeName: "ContactInfo", FieldNames: []string{ - "orderId", - "customerName", - "totalItems", - "orderLines", + "email", + "phone", + "address", }, }, { - TypeName: "OrderLine", + TypeName: "Address", FieldNames: []string{ - "productId", - "quantity", - "modifiers", + "street", + "city", + "country", + "zipCode", }, }, { - TypeName: "ActionInput", + TypeName: "CatBreed", FieldNames: []string{ + "id", "name", + "origin", + "characteristics", }, }, { - TypeName: "Product", + TypeName: "DogBreed", FieldNames: []string{ "id", "name", - "price", + "origin", + "characteristics", }, }, { - TypeName: "Storage", + TypeName: "BreedCharacteristics", + FieldNames: []string{ + "size", + "temperament", + "lifespan", + }, + }, + { + TypeName: "StorageItem", FieldNames: []string{ "id", "name", - "location", + "weight", }, }, { - TypeName: "Warehouse", + TypeName: "PalletItem", FieldNames: []string{ "id", "name", - "location", + "weight", + "palletCount", + "handler", + "specs", }, }, { - TypeName: "FilterTypeInput", + TypeName: "ContainerItem", FieldNames: []string{ - "filterField1", - "filterField2", + "id", + "name", + "weight", + "containerSize", + "handler", + "specs", }, }, { - TypeName: "FilterType", + TypeName: "ItemHandler", FieldNames: []string{ + "id", "name", - "filterField1", - "filterField2", - "pagination", + "assignedItem", }, }, { - TypeName: "Pagination", + TypeName: "PalletSpecs", FieldNames: []string{ - "page", - "perPage", + "name", + "maxWeight", + "dimensions", }, }, { - TypeName: "ComplexFilterTypeInput", + TypeName: "ContainerSpecs", FieldNames: []string{ - "filter", + "name", + "volume", + "dimensions", }, }, { - TypeName: "OrderLineInput", + TypeName: "Dimensions", FieldNames: []string{ - "productId", - "quantity", - "modifiers", + "length", + "width", + "height", }, }, { - TypeName: "OrderInput", + TypeName: "StorageSuccess", FieldNames: []string{ - "orderId", - "customerName", - "lines", + "message", + "completedAt", + }, + }, + { + TypeName: "StorageFailure", + FieldNames: []string{ + "message", + "errorCode", + }, + }, + { + TypeName: "SecuritySetup", + FieldNames: []string{ + "securityLevel", + "primaryItem", }, }, { @@ -655,43 +1387,41 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { }, }, { - TypeName: "SearchInput", + TypeName: "TestContainer", FieldNames: []string{ - "query", - "limit", + "id", + "name", + "description", + "details", }, }, { - TypeName: "SearchResult", + TypeName: "TestDetails", FieldNames: []string{ - "product", - "user", - "category", + "id", + "summary", + "pet", + "status", }, }, { - TypeName: "ActionResult", + TypeName: "SearchInput", FieldNames: []string{ - "actionSuccess", - "actionError", + "query", + "limit", }, }, { - TypeName: "NullableFieldsType", + TypeName: "ActionInput", FieldNames: []string{ - "id", - "name", - "optionalString", - "optionalInt", - "optionalFloat", - "optionalBoolean", - "requiredString", - "requiredInt", + "type", + "payload", }, }, { - TypeName: "NullableFieldsInput", + TypeName: "NullableFieldsType", FieldNames: []string{ + "id", "name", "optionalString", "optionalInt", @@ -701,14 +1431,6 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "requiredInt", }, }, - { - TypeName: "NullableFieldsFilter", - FieldNames: []string{ - "name", - "optionalString", - "includeNulls", - }, - }, { TypeName: "BlogPost", FieldNames: []string{ @@ -806,6 +1528,26 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "skillCount", }, }, + { + TypeName: "NullableFieldsInput", + FieldNames: []string{ + "name", + "optionalString", + "optionalInt", + "optionalFloat", + "optionalBoolean", + "requiredString", + "requiredInt", + }, + }, + { + TypeName: "NullableFieldsFilter", + FieldNames: []string{ + "name", + "optionalString", + "includeNulls", + }, + }, { TypeName: "CategoryInput", FieldNames: []string{ @@ -813,6 +1555,47 @@ func GetDataSourceMetadata() *plan.DataSourceMetadata { "kind", }, }, + { + TypeName: "ProductCountFilter", + FieldNames: []string{ + "minPrice", + "maxPrice", + "inStock", + "searchTerm", + }, + }, + { + TypeName: "SubcategoryItemFilter", + FieldNames: []string{ + "minPrice", + "maxPrice", + "inStock", + "isActive", + "searchTerm", + }, + }, + { + TypeName: "ShippingDestination", + FieldNames: []string{ + "DOMESTIC", + "EXPRESS", + "INTERNATIONAL", + }, + }, + { + TypeName: "ShippingEstimateInput", + FieldNames: []string{ + "destination", + "weight", + "expedited", + }, + }, + { + TypeName: "UserInput", + FieldNames: []string{ + "name", + }, + }, { TypeName: "ConditionsInput", FieldNames: []string{