Skip to content

Commit d089e70

Browse files
committed
Ensure arguments for unset fragment argument variables are removed so they execute as "unset" rather than as an invalid, undefined variable.
1 parent e27a10e commit d089e70

File tree

2 files changed

+109
-36
lines changed

2 files changed

+109
-36
lines changed

src/execution/__tests__/variables-test.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function executeQueryWithFragmentArguments(
134134
query: string,
135135
variableValues?: { [variable: string]: unknown },
136136
) {
137-
const document = parse(query, {allowFragmentArguments: true});
137+
const document = parse(query, { allowFragmentArguments: true });
138138
return executeSync({ schema, document, variableValues });
139139
}
140140

@@ -1014,21 +1014,73 @@ describe('Execute: Handles inputs', () => {
10141014
});
10151015

10161016
describe('using fragment arguments', () => {
1017-
const result = executeQueryWithFragmentArguments(`
1018-
query {
1019-
...a(value: "A")
1020-
}
1017+
it('when a value is required and provided', () => {
1018+
const result = executeQueryWithFragmentArguments(`
1019+
query {
1020+
...a(value: "A")
1021+
}
10211022
1022-
fragment a($value: String!) on TestType {
1023-
fieldWithNonNullableStringInput(input: $value)
1024-
}
1025-
`);
1026-
console.log(JSON.stringify(result));
1027-
expect(result).to.deep.equal({
1028-
data: {
1029-
fieldWithNonNullableStringInput:
1030-
'"A"',
1031-
},
1023+
fragment a($value: String!) on TestType {
1024+
fieldWithNonNullableStringInput(input: $value)
1025+
}
1026+
`);
1027+
expect(result).to.deep.equal({
1028+
data: {
1029+
fieldWithNonNullableStringInput: '"A"',
1030+
},
1031+
});
1032+
});
1033+
1034+
it('when the definition has a default and is provided', () => {
1035+
const result = executeQueryWithFragmentArguments(`
1036+
query {
1037+
...a(value: "A")
1038+
}
1039+
1040+
fragment a($value: String! = "B") on TestType {
1041+
fieldWithNonNullableStringInput(input: $value)
1042+
}
1043+
`);
1044+
expect(result).to.deep.equal({
1045+
data: {
1046+
fieldWithNonNullableStringInput: '"A"',
1047+
},
1048+
});
1049+
});
1050+
1051+
it('when the definition has a default and is not provided', () => {
1052+
const result = executeQueryWithFragmentArguments(`
1053+
query {
1054+
...a
1055+
}
1056+
1057+
fragment a($value: String! = "B") on TestType {
1058+
fieldWithNonNullableStringInput(input: $value)
1059+
}
1060+
`);
1061+
expect(result).to.deep.equal({
1062+
data: {
1063+
fieldWithNonNullableStringInput: '"B"',
1064+
},
1065+
});
1066+
});
1067+
1068+
it('when the definition has no default and is not provided', () => {
1069+
const result = executeQueryWithFragmentArguments(`
1070+
query {
1071+
...a
1072+
}
1073+
1074+
fragment a($value: String) on TestType {
1075+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $value)
1076+
}
1077+
`);
1078+
expect(result).to.deep.equal({
1079+
data: {
1080+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1081+
'"Hello World"',
1082+
},
1083+
});
10321084
});
10331085
});
10341086

src/execution/execute.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ import type {
2727
FragmentDefinitionNode,
2828
ArgumentNode,
2929
ValueNode,
30-
DirectiveNode,
3130
} from '../language/ast';
3231
import { Kind } from '../language/kinds';
33-
import { print } from '../language/printer';
32+
import { visit } from '../language/visitor';
3433

3534
import type { GraphQLSchema } from '../type/schema';
3635
import type {
@@ -70,7 +69,6 @@ import {
7069
getArgumentValues,
7170
getDirectiveValues,
7271
} from './values';
73-
import { visit } from '../language';
7472

7573
/**
7674
* Terminology
@@ -516,14 +514,15 @@ export function collectFields(
516514
) {
517515
continue;
518516
}
519-
const selectionSet = selectionSetWithFragmentArgumentsApplied(
517+
const updatedSelectionSet = selectionSetWithFragmentArgumentsApplied(
518+
exeContext,
520519
fragment,
521-
selection.arguments,
520+
selection,
522521
);
523522
collectFields(
524523
exeContext,
525524
runtimeType,
526-
selectionSet,
525+
updatedSelectionSet,
527526
fields,
528527
visitedFragmentNames,
529528
);
@@ -534,35 +533,57 @@ export function collectFields(
534533
return fields;
535534
}
536535

537-
function selectionSetWithFragmentArgumentsApplied(fragment: FragmentDefinitionNode, fragmentArguments?: ReadonlyArray<ArgumentNode>): SelectionSetNode {
536+
function selectionSetWithFragmentArgumentsApplied(
537+
exeContext: ExecutionContext,
538+
fragment: FragmentDefinitionNode,
539+
fragmentSpread: FragmentSpreadNode,
540+
): SelectionSetNode {
538541
if (fragment.variableDefinitions == null) {
539542
return fragment.selectionSet;
540543
}
541544

542545
const providedArguments: Map<string, ArgumentNode> = new Map();
543-
for (const arg of fragmentArguments ?? []) {
546+
for (const arg of fragmentSpread.arguments ?? []) {
544547
providedArguments.set(arg.name.value, arg);
545548
}
546-
const fragmentArgumentValues: Map<string, ValueNode> = new Map();
549+
const fragmentArgumentValues: Map<string, ValueNode | null> = new Map();
547550
for (const argDef of fragment.variableDefinitions ?? []) {
548-
const argName = argDef.variable.name.value;
549-
const providedArg = providedArguments.get(argName);
550-
const argDefaultValue = argDef.defaultValue;
551+
const variableName = argDef.variable.name.value;
552+
const providedArg = providedArguments.get(variableName);
551553
if (providedArg != null) {
552-
fragmentArgumentValues.set(argName, providedArg.value);
553-
} else if (argDefaultValue != null) {
554-
fragmentArgumentValues.set(argName, argDefaultValue);
554+
fragmentArgumentValues.set(variableName, providedArg.value);
555+
} else if (argDef.defaultValue != null) {
556+
fragmentArgumentValues.set(variableName, argDef.defaultValue);
557+
} else {
558+
if (isNonNullType(argDef.type)) {
559+
exeContext.errors.push(
560+
new GraphQLError(
561+
`Fragment argument "$${variableName}" on fragment "${fragment.name.value}" is required but not provided.`,
562+
[fragmentSpread, argDef],
563+
),
564+
);
565+
}
566+
fragmentArgumentValues.set(variableName, null);
555567
}
556568
}
557569

558570
return visit(fragment.selectionSet, {
559-
Variable(variable) {
560-
const replacementValue = fragmentArgumentValues.get(variable.name.value);
561-
if (replacementValue != null) {
562-
return replacementValue;
571+
Argument(node) {
572+
if (node.value.kind === Kind.VARIABLE) {
573+
const variable = node.value;
574+
if (fragmentArgumentValues.has(variable.name.value)) {
575+
const replacementValue = fragmentArgumentValues.get(
576+
variable.name.value,
577+
);
578+
if (replacementValue == null) {
579+
// Remove the argument if the provided value is unset.
580+
return null;
581+
}
582+
583+
return { ...node, value: replacementValue };
584+
}
563585
}
564-
return variable;
565-
}
586+
},
566587
});
567588
}
568589

0 commit comments

Comments
 (0)