This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git


The following commit(s) were added to refs/heads/master by this push:
     new f7b4826  SLING-13152 Fix SelectedFieldWrapper overwriting sub-field 
selections for aliased duplicate fields (#45)
f7b4826 is described below

commit f7b4826f33ade8c55e624f49e8774fd1c84cfcbd
Author: Martin Schulze <[email protected]>
AuthorDate: Tue Mar 31 16:09:40 2026 +0200

    SLING-13152 Fix SelectedFieldWrapper overwriting sub-field selections for 
aliased duplicate fields (#45)
    
    ---------
    
    Co-authored-by: Martin Schulze <[email protected]>
---
 .../graphql/core/engine/SelectedFieldWrapper.java  |  35 ++++-
 .../graphql/core/engine/SelectionSetWrapper.java   |   7 +
 .../core/engine/SelectedFieldWrapperTest.java      | 157 +++++++++++++++++++++
 3 files changed, 196 insertions(+), 3 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java 
b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
index 15399ee..9f399fb 100644
--- 
a/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
+++ 
b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
@@ -36,7 +36,9 @@ import java.util.Map;
 /**
  * Implement a wrapper for GraphQL SelectedField.
  *
- * ATTENTION: here we are assuming that fields added are unqiue by the Fully 
Qualified Name (FQN).
+ * ATTENTION: here we are assuming that fields added are unique by the Fully 
Qualified Name (FQN).
+ * When multiple children share the same FQN (e.g. aliased selections with 
different inline fragments),
+ * their sub-fields are merged into a single wrapper rather than kept as 
separate entries.
  *
  * This updated version is keeping duplicate fields by field's simple name. 
Use getFirstSubSelectedFieldByName() if
  * you are sure that there is only one field with that simple name otherwise 
use hasDuplicateFieldByName() to determine
@@ -69,14 +71,41 @@ public class SelectedFieldWrapper implements SelectedField {
         if (selectionSet != null) {
             selectionSet.getImmediateFields().forEach(sf -> {
                 SelectedFieldWrapper selectedChildField = new 
SelectedFieldWrapper(sf);
-                subFieldMap.put(sf.getName(), selectedChildField);
-                subFQNFieldMap.put(sf.getFullyQualifiedName(), 
selectedChildField);
+                String fqn = sf.getFullyQualifiedName();
+                SelectedField existing = subFQNFieldMap.get(fqn);
+                if (existing instanceof SelectedFieldWrapper) {
+                    // Merge sub-fields from duplicate entries (e.g. aliased 
selections
+                    // of the same field with different inline fragments)
+                    ((SelectedFieldWrapper) 
existing).mergeSubFields(selectedChildField);
+                } else {
+                    subFQNFieldMap.put(fqn, selectedChildField);
+                    subFieldMap.put(sf.getName(), selectedChildField);
+                }
             });
         }
         // Fields are not taken from the FQN Map to avoid dropping fields with 
the same name
         subFields = new ArrayList<>(subFQNFieldMap.values());
     }
 
+    /**
+     * Merge sub-fields from another SelectedFieldWrapper into this one.
+     * This handles the case where the same field is selected multiple times
+     * (e.g. via aliases with different inline fragments), and the 
sub-selections
+     * need to be combined.
+     */
+    void mergeSubFields(SelectedFieldWrapper other) {
+        for (SelectedField otherSub : other.getSubSelectedFields()) {
+            String fqn = otherSub.getFullyQualifiedName();
+            if (fqn != null) {
+                subFQNFieldMap.computeIfAbsent(fqn, k -> {
+                    subFieldMap.put(otherSub.getName(), otherSub);
+                    subFields.add(otherSub);
+                    return otherSub;
+                });
+            }
+        }
+    }
+
     @Override
     public String getName() {
         return name;
diff --git 
a/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java 
b/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java
index 0a97453..7e187e8 100644
--- 
a/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java
+++ 
b/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java
@@ -49,6 +49,13 @@ public class SelectionSetWrapper implements SelectionSet {
                     if (!name.contains("/")) {
                         fields.add(selectedField);
                     }
+                } else {
+                    // Merge sub-fields from duplicate field entries (e.g. 
aliased
+                    // selections of the same field with different inline 
fragments)
+                    SelectedField existing = fieldsMap.get(name);
+                    if (existing instanceof SelectedFieldWrapper) {
+                        ((SelectedFieldWrapper) 
existing).mergeSubFields(selectedField);
+                    }
                 }
             });
             initFlatMap(fields, "");
diff --git 
a/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
 
b/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
index 2dc1efe..66f1e53 100644
--- 
a/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
+++ 
b/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
@@ -179,4 +179,161 @@ public class SelectedFieldWrapperTest {
         assertNull("Second Field unexpectedly found by FQN", 
targetParent.getSubSelectedField(FIELD_SUB_SIMPLE_NAME_2));
         assertNotNull("Second Field not found by FQN", 
targetParent.getSubSelectedField(FIELD_SUB_FULLY_QUALIFIED_NAME_2));
     }
+
+    /**
+     * Tests the mergeSubFields behavior (SITES-42449): when graphql-java 
returns
+     * two immediate children with the same FQN (e.g. two "items" entries from 
aliased
+     * inline fragments), their sub-fields must be merged rather than the 
first being
+     * overwritten by the second.
+     *
+     * Simulates: { parentList { items { ... on ModelA { fieldA } ... on 
ModelB { fieldB } } } }
+     * where graphql-java produces two "items" entries with FQN 
"Parent.items", each
+     * carrying different sub-fields (ModelA.fieldA vs ModelB.fieldB).
+     */
+    @Test
+    public void testMergeSubFieldsForDuplicateFQN() {
+        // FQN shared by both "items" entries
+        String itemsFqn = "Parent.items";
+        String itemsName = "items";
+
+        // Sub-fields from the first inline fragment (ModelA)
+        String subFqnA = "ModelA.fieldA";
+        String subNameA = "fieldA";
+        graphql.schema.SelectedField sourceSubA = 
mock(graphql.schema.SelectedField.class);
+        doReturn(subNameA).when(sourceSubA).getName();
+        doReturn(subNameA).when(sourceSubA).getQualifiedName();
+        doReturn(subFqnA).when(sourceSubA).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet selSetItems1 = 
mock(DataFetchingFieldSelectionSet.class);
+        
doReturn(Arrays.asList(sourceSubA)).when(selSetItems1).getImmediateFields();
+
+        graphql.schema.SelectedField sourceItems1 = 
mock(graphql.schema.SelectedField.class);
+        doReturn(itemsName).when(sourceItems1).getName();
+        doReturn(itemsName).when(sourceItems1).getQualifiedName();
+        doReturn(itemsFqn).when(sourceItems1).getFullyQualifiedName();
+        doReturn(selSetItems1).when(sourceItems1).getSelectionSet();
+
+        // Sub-fields from the second inline fragment (ModelB)
+        String subFqnB = "ModelB.fieldB";
+        String subNameB = "fieldB";
+        graphql.schema.SelectedField sourceSubB = 
mock(graphql.schema.SelectedField.class);
+        doReturn(subNameB).when(sourceSubB).getName();
+        doReturn(subNameB).when(sourceSubB).getQualifiedName();
+        doReturn(subFqnB).when(sourceSubB).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet selSetItems2 = 
mock(DataFetchingFieldSelectionSet.class);
+        
doReturn(Arrays.asList(sourceSubB)).when(selSetItems2).getImmediateFields();
+
+        graphql.schema.SelectedField sourceItems2 = 
mock(graphql.schema.SelectedField.class);
+        doReturn(itemsName).when(sourceItems2).getName();
+        doReturn(itemsName).when(sourceItems2).getQualifiedName();
+        doReturn(itemsFqn).when(sourceItems2).getFullyQualifiedName();
+        doReturn(selSetItems2).when(sourceItems2).getSelectionSet();
+
+        // Parent field with both "items" entries as immediate children
+        graphql.schema.SelectedField sourceParent = 
mock(graphql.schema.SelectedField.class);
+        doReturn("parent").when(sourceParent).getName();
+        doReturn("parent").when(sourceParent).getQualifiedName();
+        doReturn("Query.parent").when(sourceParent).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet parentSelSet = 
mock(DataFetchingFieldSelectionSet.class);
+        doReturn(Arrays.asList(sourceItems1, 
sourceItems2)).when(parentSelSet).getImmediateFields();
+        doReturn(parentSelSet).when(sourceParent).getSelectionSet();
+
+        SelectedFieldWrapper parent = new SelectedFieldWrapper(sourceParent);
+
+        // The parent should have exactly one "items" child (merged, not 
duplicated)
+        SelectedField itemsField = parent.getSubSelectedFieldByFQN(itemsFqn);
+        assertNotNull("Items field not found by FQN", itemsField);
+
+        // The merged "items" field should contain sub-fields from BOTH inline 
fragments
+        assertTrue("Sub-field from first inline fragment (ModelA.fieldA) 
missing",
+                itemsField.hasSubSelectedFieldsByFQN(subFqnA));
+        assertTrue("Sub-field from second inline fragment (ModelB.fieldB) 
missing",
+                itemsField.hasSubSelectedFieldsByFQN(subFqnB));
+
+        // Verify by direct lookup
+        SelectedField foundA = itemsField.getSubSelectedFieldByFQN(subFqnA);
+        assertNotNull("fieldA not found by FQN", foundA);
+        assertEquals("Wrong name for fieldA", subNameA, foundA.getName());
+
+        SelectedField foundB = itemsField.getSubSelectedFieldByFQN(subFqnB);
+        assertNotNull("fieldB not found by FQN", foundB);
+        assertEquals("Wrong name for fieldB", subNameB, foundB.getName());
+
+        // Total sub-fields count should be 2 (one from each inline fragment)
+        assertEquals("Expected 2 merged sub-fields", 2, 
itemsField.getSubSelectedFields().size());
+    }
+
+    /**
+     * Verifies that when two immediate children share the same FQN (and 
simple name),
+     * the merged-away duplicate does NOT remain in subFieldMap.
+     *
+     * Before the fix, subFieldMap.put() ran unconditionally before the FQN 
dedup merge,
+     * leaving a stale wrapper in the map. This caused 
hasDuplicateFieldByName() to report
+     * a false positive and getSubSelectedFieldByName() to return both the 
merged wrapper
+     * and the stale one.
+     */
+    @Test
+    public void testNoDuplicateInSubFieldMapAfterFQNMerge() {
+        String itemsFqn = "Parent.items";
+        String itemsName = "items";
+
+        // First "items" child with one sub-field
+        graphql.schema.SelectedField sourceSubA = 
mock(graphql.schema.SelectedField.class);
+        doReturn("fieldA").when(sourceSubA).getName();
+        doReturn("fieldA").when(sourceSubA).getQualifiedName();
+        doReturn("ModelA.fieldA").when(sourceSubA).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet selSetItems1 = 
mock(DataFetchingFieldSelectionSet.class);
+        
doReturn(Arrays.asList(sourceSubA)).when(selSetItems1).getImmediateFields();
+
+        graphql.schema.SelectedField sourceItems1 = 
mock(graphql.schema.SelectedField.class);
+        doReturn(itemsName).when(sourceItems1).getName();
+        doReturn(itemsName).when(sourceItems1).getQualifiedName();
+        doReturn(itemsFqn).when(sourceItems1).getFullyQualifiedName();
+        doReturn(selSetItems1).when(sourceItems1).getSelectionSet();
+
+        // Second "items" child (same name + same FQN) with a different 
sub-field
+        graphql.schema.SelectedField sourceSubB = 
mock(graphql.schema.SelectedField.class);
+        doReturn("fieldB").when(sourceSubB).getName();
+        doReturn("fieldB").when(sourceSubB).getQualifiedName();
+        doReturn("ModelB.fieldB").when(sourceSubB).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet selSetItems2 = 
mock(DataFetchingFieldSelectionSet.class);
+        
doReturn(Arrays.asList(sourceSubB)).when(selSetItems2).getImmediateFields();
+
+        graphql.schema.SelectedField sourceItems2 = 
mock(graphql.schema.SelectedField.class);
+        doReturn(itemsName).when(sourceItems2).getName();
+        doReturn(itemsName).when(sourceItems2).getQualifiedName();
+        doReturn(itemsFqn).when(sourceItems2).getFullyQualifiedName();
+        doReturn(selSetItems2).when(sourceItems2).getSelectionSet();
+
+        // Parent with both "items" entries
+        graphql.schema.SelectedField sourceParent = 
mock(graphql.schema.SelectedField.class);
+        doReturn("parent").when(sourceParent).getName();
+        doReturn("parent").when(sourceParent).getQualifiedName();
+        doReturn("Query.parent").when(sourceParent).getFullyQualifiedName();
+
+        DataFetchingFieldSelectionSet parentSelSet = 
mock(DataFetchingFieldSelectionSet.class);
+        doReturn(Arrays.asList(sourceItems1, 
sourceItems2)).when(parentSelSet).getImmediateFields();
+        doReturn(parentSelSet).when(sourceParent).getSelectionSet();
+
+        SelectedFieldWrapper parent = new SelectedFieldWrapper(sourceParent);
+
+        // subFieldMap must contain exactly one entry for "items" (the merged 
wrapper),
+        // NOT two (merged + stale)
+        assertFalse("Same-FQN children must not be reported as duplicates by 
simple name",
+                parent.hasDuplicateFieldByName(itemsName));
+        Collection<SelectedField> byName = 
parent.getSubSelectedFieldByName(itemsName);
+        assertEquals("Expected exactly 1 entry in subFieldMap for same-FQN 
children", 1, byName.size());
+
+        // The single entry must be the merged wrapper containing sub-fields 
from both fragments
+        SelectedField merged = byName.iterator().next();
+        assertEquals("Expected 2 sub-fields in merged wrapper", 2, 
merged.getSubSelectedFields().size());
+        assertTrue("fieldA missing from merged wrapper",
+                merged.hasSubSelectedFieldsByFQN("ModelA.fieldA"));
+        assertTrue("fieldB missing from merged wrapper",
+                merged.hasSubSelectedFieldsByFQN("ModelB.fieldB"));
+    }
 }

Reply via email to