From 097e7101ae69945d1b58b98d99c1164fb22d67cf Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 11:59:47 -0700 Subject: [PATCH] fix(oe): dedupe provider queries by schema and skip empty field paths BuildProviderLevelQuery keyed only on ServiceKey, so a second SchemaID for the same provider never got a query and its fields were dropped. Also guard push*Ast against empty path segments (dot-less array paths). Closes #508 Co-authored-by: Cursor --- internal/oe/federator/builder.go | 20 ++++++--- internal/oe/federator/federator_test.go | 54 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/internal/oe/federator/builder.go b/internal/oe/federator/builder.go index 47ed99771..074184015 100644 --- a/internal/oe/federator/builder.go +++ b/internal/oe/federator/builder.go @@ -9,14 +9,15 @@ import ( func BuildProviderLevelQuery(fieldsMap *[]ProviderLevelFieldRecord) []*FederationServiceAST { var queries []*FederationServiceAST - var addedServiceKeys []string + // Dedup by service+schema: one provider may expose multiple schemaIds. + var addedQueryKeys []string for _, field := range *fieldsMap { - serviceKey := field.ServiceKey + queryKey := serviceKey + "\x00" + field.SchemaId - if !contains(addedServiceKeys, serviceKey) { - addedServiceKeys = append(addedServiceKeys, serviceKey) + if !contains(addedQueryKeys, queryKey) { + addedQueryKeys = append(addedQueryKeys, queryKey) queries = append(queries, &FederationServiceAST{ ServiceKey: serviceKey, SchemaID: field.SchemaId, @@ -38,9 +39,12 @@ func BuildProviderLevelQuery(fieldsMap *[]ProviderLevelFieldRecord) []*Federatio }, }) } - // find the query with the service key + // Attach fields to the query matching both service key and schema ID. for _, q := range queries { if q.ServiceKey == serviceKey && q.SchemaID == field.SchemaId { + if field.FieldPath == "" { + break + } args := strings.Split(field.FieldPath, ".") pushFieldToAst(args, q.QueryAst.Definitions[0].(*ast.OperationDefinition).SelectionSet) break @@ -133,6 +137,9 @@ func BuildArrayProviderQuery(fieldsMap []string, arrayFields []string) []*Federa } func pushFieldToAst(field []string, parentField *ast.SelectionSet) { + if len(field) == 0 { + return + } // loop through selectionSets to match the field path. current := field[0] @@ -202,6 +209,9 @@ func pushFieldToAst(field []string, parentField *ast.SelectionSet) { // pushArrayFieldToAst handles array fields specifically, ensuring proper array structure func pushArrayFieldToAst(field []string, parentField *ast.SelectionSet) { + if len(field) == 0 { + return + } // For array fields, we need to handle the structure differently current := field[0] diff --git a/internal/oe/federator/federator_test.go b/internal/oe/federator/federator_test.go index d6948032d..a192b9bba 100644 --- a/internal/oe/federator/federator_test.go +++ b/internal/oe/federator/federator_test.go @@ -550,6 +550,60 @@ func TestBuildProviderLevelQuery(t *testing.T) { } } +func TestBuildProviderLevelQuery_SameServiceDifferentSchemas(t *testing.T) { + fieldsMap := &[]ProviderLevelFieldRecord{ + {ServiceKey: "drp", SchemaId: "schema-v1", FieldPath: "person.fullName"}, + {ServiceKey: "drp", SchemaId: "schema-v2", FieldPath: "person.address"}, + {ServiceKey: "drp", SchemaId: "schema-v1", FieldPath: "person.nic"}, + } + + queries := BuildProviderLevelQuery(fieldsMap) + require.Len(t, queries, 2) + + bySchema := map[string]*FederationServiceAST{} + for _, q := range queries { + assert.Equal(t, "drp", q.ServiceKey) + bySchema[q.SchemaID] = q + } + require.Contains(t, bySchema, "schema-v1") + require.Contains(t, bySchema, "schema-v2") + + v1Op := bySchema["schema-v1"].QueryAst.Definitions[0].(*ast.OperationDefinition) + v2Op := bySchema["schema-v2"].QueryAst.Definitions[0].(*ast.OperationDefinition) + + // v1 got fullName + nic under person; v2 got address under person + assert.ElementsMatch(t, []string{"person"}, extractFieldNames(v1Op.SelectionSet)) + assert.ElementsMatch(t, []string{"person"}, extractFieldNames(v2Op.SelectionSet)) + + personV1 := v1Op.SelectionSet.Selections[0].(*ast.Field) + personV2 := v2Op.SelectionSet.Selections[0].(*ast.Field) + assert.ElementsMatch(t, []string{"fullName", "nic"}, extractFieldNames(personV1.SelectionSet)) + assert.ElementsMatch(t, []string{"address"}, extractFieldNames(personV2.SelectionSet)) +} + +func TestBuildProviderLevelQuery_EmptyFieldPathSkipped(t *testing.T) { + fieldsMap := &[]ProviderLevelFieldRecord{ + {ServiceKey: "drp", SchemaId: "schema1", FieldPath: ""}, + {ServiceKey: "drp", SchemaId: "schema1", FieldPath: "person.fullName"}, + } + queries := BuildProviderLevelQuery(fieldsMap) + require.Len(t, queries, 1) + op := queries[0].QueryAst.Definitions[0].(*ast.OperationDefinition) + assert.ElementsMatch(t, []string{"person"}, extractFieldNames(op.SelectionSet)) +} + +func TestBuildArrayProviderQuery_DotlessPathNoPanic(t *testing.T) { + // Path with no '.' yields empty args[1:]; must not panic. + assert.NotPanics(t, func() { + queries := BuildArrayProviderQuery([]string{"drp"}, []string{"dmt"}) + require.Len(t, queries, 2) + for _, q := range queries { + op := q.QueryAst.Definitions[0].(*ast.OperationDefinition) + assert.Empty(t, op.SelectionSet.Selections) + } + }) +} + // Helper function to extract field names from selection set func extractFieldNames(selectionSet *ast.SelectionSet) []string { var fields []string