Skip to content

Commit 4d2438a

Browse files
authored
SPEC/BUG: Ambiguity with null variable values and default values (#1274)
This change corresponds to a spec proposal (graphql/graphql-spec#418) which solves an ambiguity in how variable values and default values behave with explicit null values, and changes validation rules to better define the behavior of default values. Otherwise, this ambiguity can allows for null values to appear in non-null argument values, which may result in unforeseen null-pointer-errors. In summary this change includes: * **BREAKING:** Removal of `VariablesDefaultValueAllowed` validation rule. All variables may now specify a default value. * Change to `VariablesInAllowedPosition` rule to explicitly not allow a `null` default value when flowing into a non-null argument, and now allows optional (nullable) variables in non-null arguments that provide default values. * Changes to `ProvidedRequiredArguments` rule (**BREAKING:** renamed from `ProvidedNonNullArguments`) to no longer require values to be provided to non-null arguments which provide a default value. * Changes to `getVariableValues()` and `getArgumentValues()` to ensure a `null` value never flows into a non-null argument. * Changes to `valueFromAST()` to ensure `null` variable values do not flow into non-null types. * Adds to the `TypeInfo` API to allow referencing the expected default value at a given AST position.
1 parent 54f631f commit 4d2438a

19 files changed

+614
-317
lines changed

src/execution/__tests__/nonnull-test.js

+195
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,199 @@ describe('Execute: handles non-nullable types', () => {
486486
],
487487
},
488488
);
489+
490+
describe('Handles non-null argument', () => {
491+
const schemaWithNonNullArg = new GraphQLSchema({
492+
query: new GraphQLObjectType({
493+
name: 'Query',
494+
fields: {
495+
withNonNullArg: {
496+
type: GraphQLString,
497+
args: {
498+
cannotBeNull: {
499+
type: GraphQLNonNull(GraphQLString),
500+
},
501+
},
502+
resolve: async (_, args) => {
503+
if (typeof args.cannotBeNull === 'string') {
504+
return 'Passed: ' + args.cannotBeNull;
505+
}
506+
},
507+
},
508+
},
509+
}),
510+
});
511+
512+
it('succeeds when passed non-null literal value', async () => {
513+
const result = await execute({
514+
schema: schemaWithNonNullArg,
515+
document: parse(`
516+
query {
517+
withNonNullArg (cannotBeNull: "literal value")
518+
}
519+
`),
520+
});
521+
522+
expect(result).to.deep.equal({
523+
data: {
524+
withNonNullArg: 'Passed: literal value',
525+
},
526+
});
527+
});
528+
529+
it('succeeds when passed non-null variable value', async () => {
530+
const result = await execute({
531+
schema: schemaWithNonNullArg,
532+
document: parse(`
533+
query ($testVar: String!) {
534+
withNonNullArg (cannotBeNull: $testVar)
535+
}
536+
`),
537+
variableValues: {
538+
testVar: 'variable value',
539+
},
540+
});
541+
542+
expect(result).to.deep.equal({
543+
data: {
544+
withNonNullArg: 'Passed: variable value',
545+
},
546+
});
547+
});
548+
549+
it('succeeds when missing variable has default value', async () => {
550+
const result = await execute({
551+
schema: schemaWithNonNullArg,
552+
document: parse(`
553+
query ($testVar: String = "default value") {
554+
withNonNullArg (cannotBeNull: $testVar)
555+
}
556+
`),
557+
variableValues: {
558+
// Intentionally missing variable
559+
},
560+
});
561+
562+
expect(result).to.deep.equal({
563+
data: {
564+
withNonNullArg: 'Passed: default value',
565+
},
566+
});
567+
});
568+
569+
it('field error when missing non-null arg', async () => {
570+
// Note: validation should identify this issue first (missing args rule)
571+
// however execution should still protect against this.
572+
const result = await execute({
573+
schema: schemaWithNonNullArg,
574+
document: parse(`
575+
query {
576+
withNonNullArg
577+
}
578+
`),
579+
});
580+
581+
expect(result).to.deep.equal({
582+
data: {
583+
withNonNullArg: null,
584+
},
585+
errors: [
586+
{
587+
message:
588+
'Argument "cannotBeNull" of required type "String!" was not provided.',
589+
locations: [{ line: 3, column: 13 }],
590+
path: ['withNonNullArg'],
591+
},
592+
],
593+
});
594+
});
595+
596+
it('field error when non-null arg provided null', async () => {
597+
// Note: validation should identify this issue first (values of correct
598+
// type rule) however execution should still protect against this.
599+
const result = await execute({
600+
schema: schemaWithNonNullArg,
601+
document: parse(`
602+
query {
603+
withNonNullArg(cannotBeNull: null)
604+
}
605+
`),
606+
});
607+
608+
expect(result).to.deep.equal({
609+
data: {
610+
withNonNullArg: null,
611+
},
612+
errors: [
613+
{
614+
message:
615+
'Argument "cannotBeNull" of non-null type "String!" must ' +
616+
'not be null.',
617+
locations: [{ line: 3, column: 42 }],
618+
path: ['withNonNullArg'],
619+
},
620+
],
621+
});
622+
});
623+
624+
it('field error when non-null arg not provided variable value', async () => {
625+
// Note: validation should identify this issue first (variables in allowed
626+
// position rule) however execution should still protect against this.
627+
const result = await execute({
628+
schema: schemaWithNonNullArg,
629+
document: parse(`
630+
query ($testVar: String) {
631+
withNonNullArg(cannotBeNull: $testVar)
632+
}
633+
`),
634+
variableValues: {
635+
// Intentionally missing variable
636+
},
637+
});
638+
639+
expect(result).to.deep.equal({
640+
data: {
641+
withNonNullArg: null,
642+
},
643+
errors: [
644+
{
645+
message:
646+
'Argument "cannotBeNull" of required type "String!" was ' +
647+
'provided the variable "$testVar" which was not provided a ' +
648+
'runtime value.',
649+
locations: [{ line: 3, column: 42 }],
650+
path: ['withNonNullArg'],
651+
},
652+
],
653+
});
654+
});
655+
656+
it('field error when non-null arg provided variable with explicit null value', async () => {
657+
const result = await execute({
658+
schema: schemaWithNonNullArg,
659+
document: parse(`
660+
query ($testVar: String = "default value") {
661+
withNonNullArg (cannotBeNull: $testVar)
662+
}
663+
`),
664+
variableValues: {
665+
testVar: null,
666+
},
667+
});
668+
669+
expect(result).to.deep.equal({
670+
data: {
671+
withNonNullArg: null,
672+
},
673+
errors: [
674+
{
675+
message:
676+
'Argument "cannotBeNull" of non-null type "String!" must not be null.',
677+
locations: [{ line: 3, column: 43 }],
678+
path: ['withNonNullArg'],
679+
},
680+
],
681+
});
682+
});
683+
});
489684
});

src/execution/__tests__/variables-test.js

+105-6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ const TestType = new GraphQLObjectType({
8484
type: GraphQLString,
8585
defaultValue: 'Hello World',
8686
}),
87+
fieldWithNonNullableStringInputAndDefaultArgumentValue: fieldWithInputArg({
88+
type: GraphQLNonNull(GraphQLString),
89+
defaultValue: 'Hello World',
90+
}),
8791
fieldWithNestedInputObject: fieldWithInputArg({
8892
type: TestNestedInputObject,
8993
defaultValue: 'Hello World',
@@ -222,6 +226,40 @@ describe('Execute: Handles inputs', () => {
222226
});
223227
});
224228

229+
it('uses undefined when variable not provided', () => {
230+
const result = executeQuery(
231+
`
232+
query q($input: String) {
233+
fieldWithNullableStringInput(input: $input)
234+
}`,
235+
{
236+
// Intentionally missing variable values.
237+
},
238+
);
239+
240+
expect(result).to.deep.equal({
241+
data: {
242+
fieldWithNullableStringInput: null,
243+
},
244+
});
245+
});
246+
247+
it('uses null when variable provided explicit null value', () => {
248+
const result = executeQuery(
249+
`
250+
query q($input: String) {
251+
fieldWithNullableStringInput(input: $input)
252+
}`,
253+
{ input: null },
254+
);
255+
256+
expect(result).to.deep.equal({
257+
data: {
258+
fieldWithNullableStringInput: 'null',
259+
},
260+
});
261+
});
262+
225263
it('uses default value when not provided', () => {
226264
const result = executeQuery(`
227265
query ($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) {
@@ -236,6 +274,55 @@ describe('Execute: Handles inputs', () => {
236274
});
237275
});
238276

277+
it('does not use default value when provided', () => {
278+
const result = executeQuery(
279+
`query q($input: String = "Default value") {
280+
fieldWithNullableStringInput(input: $input)
281+
}`,
282+
{ input: 'Variable value' },
283+
);
284+
285+
expect(result).to.deep.equal({
286+
data: {
287+
fieldWithNullableStringInput: "'Variable value'",
288+
},
289+
});
290+
});
291+
292+
it('uses explicit null value instead of default value', () => {
293+
const result = executeQuery(
294+
`
295+
query q($input: String = "Default value") {
296+
fieldWithNullableStringInput(input: $input)
297+
}`,
298+
{ input: null },
299+
);
300+
301+
expect(result).to.deep.equal({
302+
data: {
303+
fieldWithNullableStringInput: 'null',
304+
},
305+
});
306+
});
307+
308+
it('uses null default value when not provided', () => {
309+
const result = executeQuery(
310+
`
311+
query q($input: String = null) {
312+
fieldWithNullableStringInput(input: $input)
313+
}`,
314+
{
315+
// Intentionally missing variable values.
316+
},
317+
);
318+
319+
expect(result).to.deep.equal({
320+
data: {
321+
fieldWithNullableStringInput: 'null',
322+
},
323+
});
324+
});
325+
239326
it('properly parses single value to list', () => {
240327
const params = { input: { a: 'foo', b: 'bar', c: 'baz' } };
241328
const result = executeQuery(doc, params);
@@ -485,8 +572,7 @@ describe('Execute: Handles inputs', () => {
485572
errors: [
486573
{
487574
message:
488-
'Variable "$value" got invalid value null; ' +
489-
'Expected non-nullable type String! not to be null.',
575+
'Variable "$value" of non-null type "String!" must not be null.',
490576
locations: [{ line: 2, column: 16 }],
491577
},
492578
],
@@ -644,8 +730,7 @@ describe('Execute: Handles inputs', () => {
644730
errors: [
645731
{
646732
message:
647-
'Variable "$input" got invalid value null; ' +
648-
'Expected non-nullable type [String]! not to be null.',
733+
'Variable "$input" of non-null type "[String]!" must not be null.',
649734
locations: [{ line: 2, column: 16 }],
650735
},
651736
],
@@ -728,8 +813,7 @@ describe('Execute: Handles inputs', () => {
728813
errors: [
729814
{
730815
message:
731-
'Variable "$input" got invalid value null; ' +
732-
'Expected non-nullable type [String!]! not to be null.',
816+
'Variable "$input" of non-null type "[String!]!" must not be null.',
733817
locations: [{ line: 2, column: 16 }],
734818
},
735819
],
@@ -853,5 +937,20 @@ describe('Execute: Handles inputs', () => {
853937
],
854938
});
855939
});
940+
941+
it('when no runtime value is provided to a non-null argument', () => {
942+
const result = executeQuery(`
943+
query optionalVariable($optional: String) {
944+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $optional)
945+
}
946+
`);
947+
948+
expect(result).to.deep.equal({
949+
data: {
950+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
951+
"'Hello World'",
952+
},
953+
});
954+
});
856955
});
857956
});

0 commit comments

Comments
 (0)