44 EnumTypeDefinitionNode ,
55 EnumValueDefinitionNode ,
66 FieldDefinitionNode ,
7+ FieldNode ,
78 GraphQLOutputType ,
89 GraphQLSchema ,
910 InputValueDefinitionNode ,
@@ -20,6 +21,7 @@ import {
2021 print ,
2122 ScalarTypeDefinitionNode ,
2223 SelectionNode ,
24+ SelectionSetNode ,
2325 TypeDefinitionNode ,
2426 TypeInfo ,
2527 UnionTypeDefinitionNode ,
@@ -28,9 +30,10 @@ import {
2830} from 'graphql' ;
2931import {
3032 delegateToSchema ,
31- extractUnavailableFields ,
33+ extractUnavailableFieldsFromSelectionSet ,
3234 MergedTypeConfig ,
3335 SubschemaConfig ,
36+ subtractSelectionSets ,
3437} from '@graphql-tools/delegate' ;
3538import { buildHTTPExecutor } from '@graphql-tools/executor-http' ;
3639import {
@@ -43,7 +46,9 @@ import {
4346} from '@graphql-tools/stitch' ;
4447import {
4548 createGraphQLError ,
49+ isPromise ,
4650 memoize1 ,
51+ mergeDeep ,
4752 parseSelectionSet ,
4853 type Executor ,
4954} from '@graphql-tools/utils' ;
@@ -109,41 +114,135 @@ export function getFieldMergerFromSupergraphSdl(
109114 return candidates [ 0 ] . fieldConfig ;
110115 }
111116 if ( candidates . some ( candidate => rootTypeMap . has ( candidate . type . name ) ) ) {
117+ const candidateNames = new Set < string > ( ) ;
118+ const realCandidates : MergeFieldConfigCandidate [ ] = [ ] ;
119+ for ( const candidate of candidates . toReversed
120+ ? candidates . toReversed ( )
121+ : [ ...candidates ] . reverse ( ) ) {
122+ if (
123+ candidate . transformedSubschema ?. name &&
124+ ! candidateNames . has ( candidate . transformedSubschema . name )
125+ ) {
126+ candidateNames . add ( candidate . transformedSubschema . name ) ;
127+ realCandidates . push ( candidate ) ;
128+ }
129+ }
130+ const defaultMergedField = defaultMerger ( candidates ) ;
112131 return {
113- ...defaultMerger ( candidates ) ,
132+ ...defaultMergedField ,
114133 resolve ( _root , _args , context , info ) {
115134 let currentSubschema : SubschemaConfig | undefined ;
116135 let currentScore = Infinity ;
117- for ( const fieldNode of info . fieldNodes ) {
118- const candidatesReversed = candidates . toReversed
119- ? candidates . toReversed ( )
120- : [ ...candidates ] . reverse ( ) ;
121- for ( const candidate of candidatesReversed ) {
122- const typeFieldMap = candidate . type . getFields ( ) ;
123- if ( candidate . transformedSubschema ) {
124- const unavailableFields = extractUnavailableFields (
125- candidate . transformedSubschema . transformedSchema ,
126- typeFieldMap [ candidate . fieldName ] ,
127- fieldNode ,
128- ( ) => true ,
136+ let currentUnavailableSelectionSet : SelectionSetNode | undefined ;
137+ let currentFriendSubschemas : Map < SubschemaConfig , SelectionSetNode > | undefined ;
138+ let currentAvailableSelectionSet : SelectionSetNode | undefined ;
139+ const originalSelectionSet : SelectionSetNode = {
140+ kind : Kind . SELECTION_SET ,
141+ selections : info . fieldNodes ,
142+ } ;
143+ // Find the best subschema to delegate this selection
144+ for ( const candidate of realCandidates ) {
145+ if ( candidate . transformedSubschema ) {
146+ const unavailableFields = extractUnavailableFieldsFromSelectionSet (
147+ candidate . transformedSubschema . transformedSchema ,
148+ candidate . type ,
149+ originalSelectionSet ,
150+ ( ) => true ,
151+ ) ;
152+ const score = calculateSelectionScore ( unavailableFields ) ;
153+ if ( score < currentScore ) {
154+ currentScore = score ;
155+ currentSubschema = candidate . transformedSubschema ;
156+ currentFriendSubschemas = new Map ( ) ;
157+ currentUnavailableSelectionSet = {
158+ kind : Kind . SELECTION_SET ,
159+ selections : unavailableFields ,
160+ } ;
161+ currentAvailableSelectionSet = subtractSelectionSets (
162+ originalSelectionSet ,
163+ currentUnavailableSelectionSet ,
129164 ) ;
130- const score = calculateSelectionScore ( unavailableFields ) ;
131- if ( score < currentScore ) {
132- currentScore = score ;
133- currentSubschema = candidate . transformedSubschema ;
165+ // Make parallel requests if there are other subschemas
166+ // that can resolve the remaining fields for this selection directly from the root field
167+ // instead of applying a type merging in advance
168+ for ( const friendCandidate of realCandidates ) {
169+ if ( friendCandidate === candidate || ! friendCandidate . transformedSubschema ) {
170+ continue ;
171+ }
172+ const unavailableFieldsInFriend = extractUnavailableFieldsFromSelectionSet (
173+ friendCandidate . transformedSubschema . transformedSchema ,
174+ friendCandidate . type ,
175+ currentUnavailableSelectionSet ,
176+ ( ) => true ,
177+ ) ;
178+ const friendScore = calculateSelectionScore ( unavailableFieldsInFriend ) ;
179+ if ( friendScore < score ) {
180+ const unavailableInFriendSelectionSet : SelectionSetNode = {
181+ kind : Kind . SELECTION_SET ,
182+ selections : unavailableFieldsInFriend ,
183+ } ;
184+ const subschemaSelectionSet = subtractSelectionSets (
185+ currentUnavailableSelectionSet ,
186+ unavailableInFriendSelectionSet ,
187+ ) ;
188+ currentFriendSubschemas . set (
189+ friendCandidate . transformedSubschema ,
190+ subschemaSelectionSet ,
191+ ) ;
192+ currentUnavailableSelectionSet = unavailableInFriendSelectionSet ;
193+ }
134194 }
135195 }
136196 }
137197 }
138198 if ( ! currentSubschema ) {
139199 throw new Error ( 'Could not determine subschema' ) ;
140200 }
141- return delegateToSchema ( {
201+ const jobs : Promise < void > [ ] = [ ] ;
202+ let hasPromise = false ;
203+ const mainJob = delegateToSchema ( {
142204 schema : currentSubschema ,
143205 operation : rootTypeMap . get ( info . parentType . name ) || ( 'query' as OperationTypeNode ) ,
144206 context,
145- info,
207+ info : currentFriendSubschemas ?. size
208+ ? {
209+ ...info ,
210+ fieldNodes : [
211+ ...( currentAvailableSelectionSet ?. selections || [ ] ) ,
212+ ...( currentUnavailableSelectionSet ?. selections || [ ] ) ,
213+ ] as FieldNode [ ] ,
214+ }
215+ : info ,
146216 } ) ;
217+ if ( isPromise ( mainJob ) ) {
218+ hasPromise = true ;
219+ }
220+ jobs . push ( mainJob ) ;
221+ if ( currentFriendSubschemas ?. size ) {
222+ for ( const [ friendSubschema , friendSelectionSet ] of currentFriendSubschemas ) {
223+ const friendJob = delegateToSchema ( {
224+ schema : friendSubschema ,
225+ operation : rootTypeMap . get ( info . parentType . name ) || ( 'query' as OperationTypeNode ) ,
226+ context,
227+ info : {
228+ ...info ,
229+ fieldNodes : friendSelectionSet . selections as FieldNode [ ] ,
230+ } ,
231+ skipTypeMerging : true ,
232+ } ) ;
233+ if ( isPromise ( friendJob ) ) {
234+ hasPromise = true ;
235+ }
236+ jobs . push ( friendJob ) ;
237+ }
238+ }
239+ if ( jobs . length === 1 ) {
240+ return jobs [ 0 ] ;
241+ }
242+ if ( hasPromise ) {
243+ return Promise . all ( jobs ) . then ( results => mergeDeep ( results ) ) ;
244+ }
245+ return mergeDeep ( jobs ) ;
147246 } ,
148247 } ;
149248 }
0 commit comments