Skip to content

Commit b69c80b

Browse files
authored
Filter selection sets recursively when finalizing gateway requests (#1301)
1 parent f4ed133 commit b69c80b

File tree

3 files changed

+214
-33
lines changed

3 files changed

+214
-33
lines changed

.changeset/clean-rules-beam.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-tools/delegate': patch
3+
---
4+
5+
Filter selection sets recursively when finalizing gateway requests
6+
7+
Because abstract types can be nested.

packages/delegate/src/finalizeGatewayRequest.ts

Lines changed: 118 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getNamedType,
1616
GraphQLField,
1717
GraphQLNamedType,
18+
GraphQLObjectType,
1819
GraphQLSchema,
1920
GraphQLType,
2021
isAbstractType,
@@ -28,6 +29,7 @@ import {
2829
OperationDefinitionNode,
2930
SelectionNode,
3031
SelectionSetNode,
32+
TypeInfo,
3133
VariableDefinitionNode,
3234
visit,
3335
visitWithTypeInfo,
@@ -483,7 +485,48 @@ function finalizeSelectionSet(
483485
const seenNonNullableMap = new WeakMap<readonly ASTNode[], Set<string>>();
484486
const seenNullableMap = new WeakMap<readonly ASTNode[], Set<string>>();
485487

486-
const filteredSelectionSet = visit(
488+
const filteredSelectionSet = filterSelectionSet(
489+
schema,
490+
typeInfo,
491+
validFragments,
492+
selectionSet,
493+
onOverlappingAliases,
494+
usedFragments,
495+
seenNonNullableMap,
496+
seenNullableMap,
497+
);
498+
499+
visit(
500+
filteredSelectionSet,
501+
{
502+
[Kind.VARIABLE]: (variableNode) => {
503+
usedVariables.push(variableNode.name.value);
504+
},
505+
},
506+
// visitorKeys argument usage a la https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
507+
// empty keys cannot be removed only because of typescript errors
508+
// will hopefully be fixed in future version of graphql-js to be optional
509+
variablesVisitorKeys as any,
510+
);
511+
512+
return {
513+
selectionSet: filteredSelectionSet,
514+
usedFragments,
515+
usedVariables,
516+
};
517+
}
518+
519+
function filterSelectionSet(
520+
schema: GraphQLSchema,
521+
typeInfo: TypeInfo,
522+
validFragments: { [name: string]: GraphQLType },
523+
selectionSet: SelectionSetNode,
524+
onOverlappingAliases: () => void,
525+
usedFragments: Array<string>,
526+
seenNonNullableMap: WeakMap<readonly ASTNode[], Set<string>>,
527+
seenNullableMap: WeakMap<readonly ASTNode[], Set<string>>,
528+
) {
529+
return visit(
487530
selectionSet,
488531
visitWithTypeInfo(typeInfo, {
489532
[Kind.FIELD]: {
@@ -541,20 +584,81 @@ function finalizeSelectionSet(
541584
}
542585
}
543586
if (possibleTypeNames.length > 0) {
544-
return possibleTypeNames.map((possibleTypeName) => ({
545-
kind: Kind.INLINE_FRAGMENT,
546-
typeCondition: {
547-
kind: Kind.NAMED_TYPE,
548-
name: {
549-
kind: Kind.NAME,
550-
value: possibleTypeName,
587+
const spreads = possibleTypeNames.map((possibleTypeName) => {
588+
if (!node.selectionSet?.selections) {
589+
// leaf field, no selection set. return as is we're sure it exists
590+
return {
591+
kind: Kind.INLINE_FRAGMENT,
592+
typeCondition: {
593+
kind: Kind.NAMED_TYPE,
594+
name: {
595+
kind: Kind.NAME,
596+
value: possibleTypeName,
597+
},
598+
},
599+
selectionSet: {
600+
kind: Kind.SELECTION_SET,
601+
selections: [node],
602+
},
603+
};
604+
}
605+
606+
// object field with selection set. filter it recursively
607+
const possibleType = schema.getType(
608+
possibleTypeName,
609+
) as GraphQLObjectType; // it's an object type because union members must be objects
610+
611+
const possibleField = possibleType.getFields()[node.name.value];
612+
if (!possibleField) {
613+
// the field does not exist on the possible type, skip the spread altogether
614+
return undefined;
615+
}
616+
617+
// recursively filter the selection set because abstract types can be nested
618+
const fieldFilteredSelectionSet = filterSelectionSet(
619+
schema,
620+
getTypeInfoWithType(schema, possibleField.type),
621+
validFragments,
622+
node.selectionSet,
623+
onOverlappingAliases,
624+
usedFragments,
625+
seenNonNullableMap,
626+
seenNullableMap,
627+
);
628+
629+
if (!fieldFilteredSelectionSet.selections.length) {
630+
// no selections remain after filtering the field, skip the spread altogether
631+
return undefined;
632+
}
633+
634+
return {
635+
kind: Kind.INLINE_FRAGMENT,
636+
typeCondition: {
637+
kind: Kind.NAMED_TYPE,
638+
name: {
639+
kind: Kind.NAME,
640+
value: possibleTypeName,
641+
},
551642
},
552-
},
553-
selectionSet: {
554-
kind: Kind.SELECTION_SET,
555-
selections: [node],
556-
},
557-
}));
643+
selectionSet: {
644+
kind: Kind.SELECTION_SET,
645+
selections: [
646+
{
647+
...node,
648+
selectionSet: fieldFilteredSelectionSet,
649+
},
650+
],
651+
},
652+
};
653+
});
654+
const nonEmptySpreads = spreads.filter(Boolean);
655+
if (!nonEmptySpreads.length) {
656+
// no spreads remain after filtering, skip the field altogether.
657+
// this is important to avoid invalid ast nodes causing empty lines
658+
// in the resulting query
659+
return undefined;
660+
}
661+
return nonEmptySpreads;
558662
}
559663
}
560664
return undefined;
@@ -729,25 +833,6 @@ function finalizeSelectionSet(
729833
// will hopefully be fixed in future version of graphql-js to be optional
730834
filteredSelectionSetVisitorKeys as any,
731835
);
732-
733-
visit(
734-
filteredSelectionSet,
735-
{
736-
[Kind.VARIABLE]: (variableNode) => {
737-
usedVariables.push(variableNode.name.value);
738-
},
739-
},
740-
// visitorKeys argument usage a la https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
741-
// empty keys cannot be removed only because of typescript errors
742-
// will hopefully be fixed in future version of graphql-js to be optional
743-
variablesVisitorKeys as any,
744-
);
745-
746-
return {
747-
selectionSet: filteredSelectionSet,
748-
usedFragments,
749-
usedVariables,
750-
};
751836
}
752837

753838
function union(...arrays: Array<Array<string>>): Array<string> {

packages/delegate/tests/finalizeGatewayRequest.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,5 +226,94 @@ describe('finalizeGatewayRequest', () => {
226226
}
227227
}`);
228228
});
229+
it('should remove fields that dont exist in schema', () => {
230+
const query = parse(/* GraphQL */ `
231+
query foo {
232+
foo {
233+
name
234+
nickname
235+
}
236+
}
237+
`);
238+
const filteredQuery = finalizeGatewayRequest(
239+
{
240+
document: query,
241+
},
242+
{
243+
targetSchema,
244+
} as DelegationContext,
245+
() => {},
246+
);
247+
expect(print(filteredQuery.document)).toMatchInlineSnapshot(`
248+
"query foo {
249+
foo {
250+
__typename
251+
... on Bar {
252+
name
253+
}
254+
}
255+
}"
256+
`);
257+
});
258+
it('should remove nested fields that dont exist in schema', () => {
259+
const query = parse(/* GraphQL */ `
260+
query foo {
261+
foo {
262+
name {
263+
first
264+
nickname
265+
}
266+
}
267+
}
268+
`);
269+
const filteredQuery = finalizeGatewayRequest(
270+
{
271+
document: query,
272+
},
273+
{
274+
targetSchema,
275+
} as DelegationContext,
276+
() => {},
277+
);
278+
expect(print(filteredQuery.document)).toMatchInlineSnapshot(`
279+
"query foo {
280+
foo {
281+
__typename
282+
... on Baz {
283+
name {
284+
first
285+
}
286+
}
287+
}
288+
}"
289+
`);
290+
});
291+
it('should remove fields whose nested fields dont exist in schema', () => {
292+
const query = parse(/* GraphQL */ `
293+
query foo {
294+
foo {
295+
name {
296+
nickname
297+
}
298+
}
299+
}
300+
`);
301+
const filteredQuery = finalizeGatewayRequest(
302+
{
303+
document: query,
304+
},
305+
{
306+
targetSchema,
307+
} as DelegationContext,
308+
() => {},
309+
);
310+
expect(print(filteredQuery.document)).toMatchInlineSnapshot(`
311+
"query foo {
312+
foo {
313+
__typename
314+
}
315+
}"
316+
`);
317+
});
229318
});
230319
});

0 commit comments

Comments
 (0)