@@ -46,6 +46,7 @@ import {
4646 sameDirectiveApplication ,
4747 isLeafType ,
4848 Variables ,
49+ isObjectType ,
4950} from "./definitions" ;
5051import { ERRORS } from "./error" ;
5152import { isDirectSubtype , sameType } from "./types" ;
@@ -750,6 +751,21 @@ export class Operation {
750751 ) ;
751752 }
752753
754+ trimUnsatisfiableBranches ( ) : Operation {
755+ const trimmedSelections = this . selectionSet . trimUnsatisfiableBranches ( this . selectionSet . parentType ) ;
756+ if ( trimmedSelections === this . selectionSet ) {
757+ return this ;
758+ }
759+
760+ return new Operation (
761+ this . schema ,
762+ this . rootKind ,
763+ trimmedSelections ,
764+ this . variableDefinitions ,
765+ this . name
766+ ) ;
767+ }
768+
753769 /**
754770 * Returns this operation but potentially modified so all/some of the @defer applications have been removed.
755771 *
@@ -971,6 +987,14 @@ export class NamedFragments {
971987 return this . fragments . values ( ) ;
972988 }
973989
990+ map ( mapper : ( def : NamedFragmentDefinition ) => NamedFragmentDefinition ) : NamedFragments {
991+ const mapped = new NamedFragments ( ) ;
992+ for ( const def of this . fragments . values ( ) ) {
993+ mapped . fragments . set ( def . name , mapper ( def ) ) ;
994+ }
995+ return mapped ;
996+ }
997+
974998 validate ( variableDefinitions : VariableDefinitions ) {
975999 for ( const fragment of this . fragments . values ( ) ) {
9761000 fragment . selectionSet . validate ( variableDefinitions ) ;
@@ -1125,19 +1149,23 @@ export class SelectionSet {
11251149 // without any fragments, we don't bother handling this more complex case.
11261150 assert ( ! this . fragments || this . fragments . isEmpty ( ) , `Should not be called on selection that already has named fragments, but got ${ this . fragments } ` )
11271151
1128- return this . lazyMap ( ( selection ) => selection . optimize ( fragments ) , fragments ) ;
1152+ return this . lazyMap ( ( selection ) => selection . optimize ( fragments ) , { fragments } ) ;
11291153 }
11301154
11311155 expandAllFragments ( ) : SelectionSet {
1132- return this . lazyMap ( ( selection ) => selection . expandAllFragments ( ) , null ) ;
1156+ return this . lazyMap ( ( selection ) => selection . expandAllFragments ( ) , { fragments : null } ) ;
11331157 }
11341158
11351159 expandFragments ( names : string [ ] , updatedFragments : NamedFragments | undefined ) : SelectionSet {
11361160 if ( names . length === 0 ) {
11371161 return this ;
11381162 }
11391163
1140- return this . lazyMap ( ( selection ) => selection . expandFragments ( names , updatedFragments ) , updatedFragments ?? null ) ;
1164+ return this . lazyMap ( ( selection ) => selection . expandFragments ( names , updatedFragments ) , { fragments : updatedFragments ?? null } ) ;
1165+ }
1166+
1167+ trimUnsatisfiableBranches ( parentType : CompositeType ) : SelectionSet {
1168+ return this . lazyMap ( ( selection ) => selection . trimUnsatisfiableBranches ( parentType ) , { parentType } ) ;
11411169 }
11421170
11431171 /**
@@ -1150,9 +1178,13 @@ export class SelectionSet {
11501178 */
11511179 lazyMap (
11521180 mapper : ( selection : Selection ) => Selection | readonly Selection [ ] | SelectionSet | undefined ,
1153- updatedFragments ?: NamedFragments | null ,
1181+ options ?: {
1182+ fragments ?: NamedFragments | null ,
1183+ parentType ?: CompositeType ,
1184+ }
11541185 ) : SelectionSet {
11551186 const selections = this . selections ( ) ;
1187+ const updatedFragments = options ?. fragments ;
11561188 const newFragments = updatedFragments === undefined ? this . fragments : ( updatedFragments ?? undefined ) ;
11571189
11581190 let updatedSelections : SelectionSetUpdates | undefined = undefined ;
@@ -1172,7 +1204,7 @@ export class SelectionSet {
11721204 if ( ! updatedSelections ) {
11731205 return this . withUpdatedFragments ( newFragments ) ;
11741206 }
1175- return updatedSelections . toSelectionSet ( this . parentType , newFragments ) ;
1207+ return updatedSelections . toSelectionSet ( options ?. parentType ?? this . parentType , newFragments ) ;
11761208 }
11771209
11781210 private withUpdatedFragments ( newFragments : NamedFragments | undefined ) : SelectionSet {
@@ -1189,6 +1221,10 @@ export class SelectionSet {
11891221 return this . lazyMap ( ( selection ) => selection . withNormalizedDefer ( normalizer ) ) ;
11901222 }
11911223
1224+ hasDefer ( ) : boolean {
1225+ return this . selections ( ) . some ( ( s ) => s . hasDefer ( ) ) ;
1226+ }
1227+
11921228 /**
11931229 * Returns the selection select from filtering out any selection that does not match the provided predicate.
11941230 *
@@ -1355,6 +1391,18 @@ export class SelectionSet {
13551391 }
13561392 }
13571393
1394+ /**
1395+ * Returns true if any of the element in this selection set matches the provided predicate.
1396+ */
1397+ some ( predicate : ( elt : OperationElement ) => boolean ) : boolean {
1398+ for ( const selection of this . selections ( ) ) {
1399+ if ( predicate ( selection . element ) || ( selection . selectionSet && selection . selectionSet . some ( predicate ) ) ) {
1400+ return true ;
1401+ }
1402+ }
1403+ return false ;
1404+ }
1405+
13581406 toOperationString (
13591407 rootKind : SchemaRootKind ,
13601408 variableDefinitions : VariableDefinitions ,
@@ -1766,7 +1814,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
17661814 return this . withUpdatedComponents ( element , this . selectionSet ) ;
17671815 }
17681816
1769- protected mapToSelectionSet ( mapper : ( s : SelectionSet ) => SelectionSet ) : TOwnType {
1817+ mapToSelectionSet ( mapper : ( s : SelectionSet ) => SelectionSet ) : TOwnType {
17701818 if ( ! this . selectionSet ) {
17711819 return this . us ( ) ;
17721820 }
@@ -1781,9 +1829,13 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
17811829
17821830 abstract withNormalizedDefer ( normalizer : DeferNormalizer ) : TOwnType | SelectionSet ;
17831831
1832+ abstract hasDefer ( ) : boolean ;
1833+
17841834 abstract expandAllFragments ( ) : TOwnType | readonly Selection [ ] ;
17851835
17861836 abstract expandFragments ( names : string [ ] , updatedFragments : NamedFragments | undefined ) : TOwnType | readonly Selection [ ] ;
1837+
1838+ abstract trimUnsatisfiableBranches ( parentType : CompositeType ) : TOwnType | SelectionSet | undefined ;
17871839}
17881840
17891841export class FieldSelection extends AbstractSelection < Field < any > , undefined , FieldSelection > {
@@ -1953,10 +2005,39 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
19532005 return this . mapToSelectionSet ( ( s ) => s . withNormalizedDefer ( normalizer ) ) ;
19542006 }
19552007
2008+ hasDefer ( ) : boolean {
2009+ return ! ! this . selectionSet ?. hasDefer ( ) ;
2010+ }
2011+
19562012 expandAllFragments ( ) : FieldSelection {
19572013 return this . mapToSelectionSet ( ( s ) => s . expandAllFragments ( ) ) ;
19582014 }
19592015
2016+ trimUnsatisfiableBranches ( _ : CompositeType ) : FieldSelection {
2017+ if ( ! this . selectionSet ) {
2018+ return this ;
2019+ }
2020+
2021+ const base = baseType ( this . element . definition . type ! )
2022+ assert ( isCompositeType ( base ) , ( ) => `Field ${ this . element } should not have a sub-selection` ) ;
2023+ const trimmed = this . mapToSelectionSet ( ( s ) => s . trimUnsatisfiableBranches ( base ) ) ;
2024+ // In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
2025+ // sub-selection is empty. Which suggest something may be wrong with this part of the query
2026+ // intent, but the query was valid while keeping an empty sub-selection isn't. So in that
2027+ // case, we just add some "non-included" __typename field just to keep the query valid.
2028+ if ( trimmed . selectionSet ?. isEmpty ( ) ) {
2029+ return trimmed . withUpdatedSelectionSet ( selectionSetOfElement (
2030+ new Field (
2031+ base . typenameField ( ) ! ,
2032+ undefined ,
2033+ [ new Directive ( 'include' , { 'if' : false } ) ] ,
2034+ )
2035+ ) ) ;
2036+ } else {
2037+ return trimmed ;
2038+ }
2039+ }
2040+
19602041 expandFragments ( names : string [ ] , updatedFragments : NamedFragments | undefined ) : FieldSelection {
19612042 return this . mapToSelectionSet ( ( s ) => s . expandFragments ( names , updatedFragments ) ) ;
19622043 }
@@ -2021,6 +2102,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
20212102
20222103 return predicate ( thisWithFilteredSelectionSet ) ? thisWithFilteredSelectionSet : undefined ;
20232104 }
2105+
2106+ hasDefer ( ) : boolean {
2107+ return this . element . hasDefer ( ) || this . selectionSet . hasDefer ( ) ;
2108+ }
20242109
20252110 equals ( that : Selection ) : boolean {
20262111 if ( this === that ) {
@@ -2179,6 +2264,87 @@ class InlineFragmentSelection extends FragmentSelection {
21792264 : this . withUpdatedComponents ( newElement , newSelection ) ;
21802265 }
21812266
2267+ trimUnsatisfiableBranches ( currentType : CompositeType ) : FragmentSelection | SelectionSet | undefined {
2268+ const thisCondition = this . element . typeCondition ;
2269+ // Note that if the condition has directives, we preserve the fragment no matter what.
2270+ if ( this . element . appliedDirectives . length === 0 ) {
2271+ if ( ! thisCondition || currentType === this . element . typeCondition ) {
2272+ const trimmed = this . selectionSet . trimUnsatisfiableBranches ( currentType ) ;
2273+ return trimmed . isEmpty ( ) ? undefined : trimmed ;
2274+ }
2275+
2276+ // If the current type is an object, then we never need to keep the current fragment because:
2277+ // - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
2278+ // so this is just an unsatisfiable branch.
2279+ // - or it's not an object, but then the current type is more precise and no poitn in "casting" to a
2280+ // less precise interface/union.
2281+ if ( isObjectType ( currentType ) ) {
2282+ if ( isObjectType ( thisCondition ) ) {
2283+ return undefined ;
2284+ } else {
2285+ const trimmed = this . selectionSet . trimUnsatisfiableBranches ( currentType ) ;
2286+ return trimmed . isEmpty ( ) ? undefined : trimmed ;
2287+ }
2288+ }
2289+ }
2290+
2291+ // In all other cases, we first recurse on the sub-selection.
2292+ const trimmedSelectionSet = this . selectionSet . trimUnsatisfiableBranches ( this . element . typeCondition ?? this . parentType ) ;
2293+
2294+ // First, could be that everything was unsatisfiable.
2295+ if ( trimmedSelectionSet . isEmpty ( ) ) {
2296+ if ( this . element . appliedDirectives . length === 0 ) {
2297+ return undefined ;
2298+ } else {
2299+ return this . withUpdatedSelectionSet ( selectionSetOfElement (
2300+ new Field (
2301+ ( this . element . typeCondition ?? this . parentType ) . typenameField ( ) ! ,
2302+ undefined ,
2303+ [ new Directive ( 'include' , { 'if' : false } ) ] ,
2304+ )
2305+ ) ) ;
2306+ }
2307+ }
2308+
2309+ // Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
2310+ // 1. the current fragment is an abstract type,
2311+ // 2. the sub-fragment is an object type,
2312+ // 3. the sub-fragment type is a valid runtime of the current type.
2313+ if ( this . element . appliedDirectives . length === 0 && isAbstractType ( thisCondition ! ) ) {
2314+ assert ( ! isObjectType ( currentType ) , ( ) => `Should not have got here if ${ currentType } is an object type` ) ;
2315+ const currentRuntimes = possibleRuntimeTypes ( currentType ) ;
2316+ const liftableSelections : Selection [ ] = [ ] ;
2317+ for ( const selection of trimmedSelectionSet . selections ( ) ) {
2318+ if ( selection . kind === 'FragmentSelection'
2319+ && selection . element . typeCondition
2320+ && isObjectType ( selection . element . typeCondition )
2321+ && currentRuntimes . includes ( selection . element . typeCondition )
2322+ ) {
2323+ liftableSelections . push ( selection ) ;
2324+ }
2325+ }
2326+
2327+ // If we can lift all selections, then that just mean we can get rid of the current fragment altogether
2328+ if ( liftableSelections . length === trimmedSelectionSet . selections ( ) . length ) {
2329+ return trimmedSelectionSet ;
2330+ }
2331+
2332+ // Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
2333+ // and the current fragment _without_ those lifted selections.
2334+ if ( liftableSelections . length > 0 ) {
2335+ const newSet = new SelectionSetUpdates ( ) ;
2336+ newSet . add ( liftableSelections ) ;
2337+ newSet . add ( this . withUpdatedSelectionSet (
2338+ trimmedSelectionSet . filter ( ( s ) => ! liftableSelections . includes ( s ) ) ,
2339+ ) ) ;
2340+ return newSet . toSelectionSet ( this . parentType ) ;
2341+ }
2342+ }
2343+
2344+ return this . selectionSet === trimmedSelectionSet ? this : this . withUpdatedSelectionSet ( trimmedSelectionSet ) ;
2345+ }
2346+
2347+
21822348 expandAllFragments ( ) : FragmentSelection {
21832349 return this . mapToSelectionSet ( ( s ) => s . expandAllFragments ( ) ) ;
21842350 }
@@ -2227,6 +2393,10 @@ class FragmentSpreadSelection extends FragmentSelection {
22272393 assert ( false , `Unsupported` ) ;
22282394 }
22292395
2396+ trimUnsatisfiableBranches ( _ : CompositeType ) : FragmentSelection {
2397+ return this ;
2398+ }
2399+
22302400 namedFragments ( ) : NamedFragments | undefined {
22312401 return this . fragments ;
22322402 }
0 commit comments