@@ -46,6 +46,7 @@ import {
46
46
isLeafType ,
47
47
Variables ,
48
48
isObjectType ,
49
+ NamedType ,
49
50
} from "./definitions" ;
50
51
import { isInterfaceObjectType } from "./federation" ;
51
52
import { ERRORS } from "./error" ;
@@ -152,7 +153,11 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
152
153
}
153
154
154
155
isLeafField ( ) : boolean {
155
- return isLeafType ( baseType ( this . definition . type ! ) ) ;
156
+ return isLeafType ( this . baseType ( ) ) ;
157
+ }
158
+
159
+ baseType ( ) : NamedType {
160
+ return baseType ( this . definition . type ! ) ;
156
161
}
157
162
158
163
withUpdatedDefinition ( newDefinition : FieldDefinition < any > ) : Field < TArgs > {
@@ -674,7 +679,7 @@ export function concatOperationPaths(head: OperationPath, tail: OperationPath):
674
679
675
680
function isUselessFollowupElement ( first : OperationElement , followup : OperationElement , conditionals : Directive < any , any > [ ] ) : boolean {
676
681
const typeOfFirst = first . kind === 'Field'
677
- ? baseType ( first . definition . type ! )
682
+ ? first . baseType ( )
678
683
: first . typeCondition ;
679
684
680
685
// The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
@@ -1430,7 +1435,73 @@ export class SelectionSet {
1430
1435
for ( const selection of selections ) {
1431
1436
mergedSubselections . add ( selection . selectionSet ! ) ;
1432
1437
}
1433
- return first . withUpdatedSelectionSet ( mergedSubselections . toSelectionSet ( first . selectionSet . parentType ) ) ;
1438
+
1439
+ // We know all the `selections` are basically for the same element (same field or same inline fragment),
1440
+ // and we want to return a single selection with the merged selections. There is a subtlety regarding
1441
+ // the parent type of that merged selection however: we cannot safely rely on the parent type of any
1442
+ // of the individual selections, because this can be incorrect. Let's illustrate.
1443
+ // Consider that we have:
1444
+ // ```graphql
1445
+ // type Query {
1446
+ // a: A!
1447
+ // }
1448
+ //
1449
+ // interface IA1 {
1450
+ // b: IB1!
1451
+ // }
1452
+ //
1453
+ // interface IA2 {
1454
+ // b: IB2!
1455
+ // }
1456
+ //
1457
+ // type A implements IA1 & IA2 {
1458
+ // b: B!
1459
+ // }
1460
+ //
1461
+ // interface IB1 {
1462
+ // v1: Int!
1463
+ // }
1464
+ //
1465
+ // interface IB2 {
1466
+ // v2: Int!
1467
+ // }
1468
+ //
1469
+ // type B implements IB1 & IB2 {
1470
+ // v1: Int!
1471
+ // v2: Int!
1472
+ // }
1473
+ // ```
1474
+ // and suppose that we're trying to check if selection set:
1475
+ // maybeSuperset = { ... on IA1 { b { v1 } } ... on IA2 { b { v2 } } } // (parent type A)
1476
+ // contains selection set:
1477
+ // maybeSubset = { b { v1 v2 } } // (parent type A)
1478
+ //
1479
+ // In that case, the `contains` method below will call this function with the 2 sub-selections
1480
+ // from `maybeSuperset`, but with the unecessary interface fragment removed (reminder that the
1481
+ // parent type is `A`, so the "casts" into the interfaces are semantically useless).
1482
+ //
1483
+ // And so in that case, the argument to this method will be:
1484
+ // [ b { v1 } (parent type IA1), b { v2 } (parent type IA2) ]
1485
+ // but then, the sub-selection `{ v1 }` of the 1st value will have parent type IB1,
1486
+ // and the sub-selection `{ v2 }` of the 2nd value will have parent type IB2,
1487
+ // neither of which work for the merge sub-selection.
1488
+ //
1489
+ // Instead, we want to use as parent type the type of field `b` the parent type of `this`
1490
+ // (which is `maybeSupeset` in our example). Which means that we want to use type `B` for
1491
+ // the sub-selection, which is now guaranteed to work (or `maybeSupergerset` wouldn't have
1492
+ // been valid).
1493
+ //
1494
+ // Long story short, we get that type by rebasing any of the selection element (we use the
1495
+ // first as we have it) on `this.parentType`, which gives use the element we want, and we
1496
+ // use the type of that for the sub-selection.
1497
+
1498
+ if ( first . kind === 'FieldSelection' ) {
1499
+ const rebasedField = first . element . rebaseOn ( this . parentType ) ;
1500
+ return new FieldSelection ( rebasedField , mergedSubselections . toSelectionSet ( rebasedField . baseType ( ) as CompositeType ) ) ;
1501
+ } else {
1502
+ const rebasedFragment = first . element . rebaseOn ( this . parentType ) ;
1503
+ return new InlineFragmentSelection ( rebasedFragment , mergedSubselections . toSelectionSet ( rebasedFragment . castedType ( ) ) ) ;
1504
+ }
1434
1505
}
1435
1506
1436
1507
contains ( that : SelectionSet ) : boolean {
@@ -1790,7 +1861,7 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
1790
1861
}
1791
1862
1792
1863
const element = updateElement ( first ) . rebaseOn ( parentType ) ;
1793
- const subSelectionParentType = element . kind === 'Field' ? baseType ( element . definition . type ! ) : element . castedType ( ) ;
1864
+ const subSelectionParentType = element . kind === 'Field' ? element . baseType ( ) : element . castedType ( ) ;
1794
1865
if ( ! isCompositeType ( subSelectionParentType ) ) {
1795
1866
// This is a leaf, so all updates should correspond ot the same field and we just use the first.
1796
1867
return selectionOfElement ( element ) ;
@@ -2120,7 +2191,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2120
2191
2121
2192
optimize ( fragments : NamedFragments ) : Selection {
2122
2193
let optimizedSelection = this . selectionSet ? this . selectionSet . optimizeSelections ( fragments ) : undefined ;
2123
- const fieldBaseType = baseType ( this . element . definition . type ! ) ;
2194
+ const fieldBaseType = this . element . baseType ( ) ;
2124
2195
if ( isCompositeType ( fieldBaseType ) && optimizedSelection ) {
2125
2196
const optimized = this . tryOptimizeSubselectionWithFragments ( {
2126
2197
parentType : fieldBaseType ,
@@ -2213,7 +2284,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2213
2284
return this . withUpdatedElement ( rebasedElement ) ;
2214
2285
}
2215
2286
2216
- const rebasedBase = baseType ( rebasedElement . definition . type ! ) ;
2287
+ const rebasedBase = rebasedElement . baseType ( ) ;
2217
2288
if ( rebasedBase === this . selectionSet . parentType ) {
2218
2289
return this . withUpdatedElement ( rebasedElement ) ;
2219
2290
}
@@ -2279,7 +2350,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2279
2350
return this ;
2280
2351
}
2281
2352
2282
- const base = baseType ( this . element . definition . type ! )
2353
+ const base = this . element . baseType ( )
2283
2354
assert ( isCompositeType ( base ) , ( ) => `Field ${ this . element } should not have a sub-selection` ) ;
2284
2355
const trimmed = ( options ?. recursive ?? true ) ? this . mapToSelectionSet ( ( s ) => s . trimUnsatisfiableBranches ( base ) ) : this ;
2285
2356
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
0 commit comments