Skip to content

Commit

Permalink
Input Value Validation
Browse files Browse the repository at this point in the history
Depends on #3065

Factors out input validation to reusable functions:

* Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`.
* Introduces `validateInputValue` by extracting this behavior from `coerceInputValue`
* Simplifies `coerceInputValue` to return early on validation error
* Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved.

These two parallel functions will be used to validate default values in #3049
  • Loading branch information
leebyron committed Jun 1, 2021
1 parent 30deb95 commit 757931d
Show file tree
Hide file tree
Showing 14 changed files with 1,459 additions and 663 deletions.
6 changes: 3 additions & 3 deletions src/execution/__tests__/nonnull-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ describe('Execute: handles non-nullable types', () => {
errors: [
{
message:
'Argument Query.withNonNullArg(cannotBeNull:) of non-null type String! must not be null.',
'Argument Query.withNonNullArg(cannotBeNull:) has invalid value: Expected value of non-null type String! not to be null.',
locations: [{ line: 3, column: 42 }],
path: ['withNonNullArg'],
},
Expand Down Expand Up @@ -673,7 +673,7 @@ describe('Execute: handles non-nullable types', () => {
errors: [
{
message:
'Argument Query.withNonNullArg(cannotBeNull:) of required type String! was provided the variable "$testVar" which was not provided a runtime value.',
'Argument Query.withNonNullArg(cannotBeNull:) has invalid value: Expected variable "$testVar" provided to type String! to provide a runtime value.',
locations: [{ line: 3, column: 42 }],
path: ['withNonNullArg'],
},
Expand Down Expand Up @@ -701,7 +701,7 @@ describe('Execute: handles non-nullable types', () => {
errors: [
{
message:
'Argument Query.withNonNullArg(cannotBeNull:) of non-null type String! must not be null.',
'Argument Query.withNonNullArg(cannotBeNull:) has invalid value: Expected variable "$testVar" provided to non-null type String! not to be null.',
locations: [{ line: 3, column: 43 }],
path: ['withNonNullArg'],
},
Expand Down
34 changes: 17 additions & 17 deletions src/execution/__tests__/variables-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Argument TestType.fieldWithObjectInput(input:) of type TestInputObject has invalid value ["foo", "bar", "baz"].',
'Argument TestType.fieldWithObjectInput(input:) has invalid value: Expected value of type TestInputObject to be an object, found: ["foo", "bar", "baz"].',
path: ['fieldWithObjectInput'],
locations: [{ line: 3, column: 41 }],
},
Expand Down Expand Up @@ -373,7 +373,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value null at "input.c"; Expected non-nullable type "String!" not to be null.',
'Variable "$input" has invalid value at .c: Expected value of non-null type String! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand All @@ -387,7 +387,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value "foo bar"; Expected type "TestInputObject" to be an object.',
'Variable "$input" has invalid value: Expected value of type TestInputObject to be an object, found: "foo bar".',
locations: [{ line: 2, column: 16 }],
},
],
Expand All @@ -401,7 +401,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value { a: "foo", b: "bar" }; Field "c" of required type "String!" was not provided.',
'Variable "$input" has invalid value: Expected value of type TestInputObject to include required field "c", found: { a: "foo", b: "bar" }.',
locations: [{ line: 2, column: 16 }],
},
],
Expand All @@ -420,12 +420,12 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value { a: "foo" } at "input.na"; Field "c" of required type "String!" was not provided.',
'Variable "$input" has invalid value at .na: Expected value of type TestInputObject to include required field "c", found: { a: "foo" }.',
locations: [{ line: 2, column: 18 }],
},
{
message:
'Variable "$input" got invalid value { na: { a: "foo" } }; Field "nb" of required type "String!" was not provided.',
'Variable "$input" has invalid value: Expected value of type TestNestedInputObject to include required field "nb", found: { na: { a: "foo" } }.',
locations: [{ line: 2, column: 18 }],
},
],
Expand All @@ -442,7 +442,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value { a: "foo", b: "bar", c: "baz", extra: "dog" }; Field "extra" is not defined by type "TestInputObject".',
'Variable "$input" has invalid value: Expected value of type TestInputObject not to include unknown field "extra", found: { a: "foo", b: "bar", c: "baz", extra: "dog" }.',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -617,7 +617,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$value" of required type String! was not provided.',
'Variable "$value" has invalid value: Expected a value of non-null type String! to be provided.',
locations: [{ line: 2, column: 16 }],
},
],
Expand All @@ -636,7 +636,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$value" of non-null type String! must not be null.',
'Variable "$value" has invalid value: Expected value of non-null type String! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -702,7 +702,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$value" got invalid value [1, 2, 3]; String cannot represent a non string value: [1, 2, 3]',
'Variable "$value" has invalid value: String cannot represent a non string value: [1, 2, 3]',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -730,7 +730,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Argument TestType.fieldWithNonNullableStringInput(input:) of required type String! was provided the variable "$foo" which was not provided a runtime value.',
'Argument TestType.fieldWithNonNullableStringInput(input:) has invalid value: Expected variable "$foo" provided to type String! to provide a runtime value.',
locations: [{ line: 3, column: 50 }],
path: ['fieldWithNonNullableStringInput'],
},
Expand Down Expand Up @@ -785,7 +785,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" of non-null type [String]! must not be null.',
'Variable "$input" has invalid value: Expected value of non-null type [String]! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -848,7 +848,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.',
'Variable "$input" has invalid value at [1]: Expected value of non-null type String! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand All @@ -867,7 +867,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" of non-null type [String!]! must not be null.',
'Variable "$input" has invalid value: Expected value of non-null type [String!]! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -897,7 +897,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.',
'Variable "$input" has invalid value at [1]: Expected value of non-null type String! not to be null.',
locations: [{ line: 2, column: 16 }],
},
],
Expand Down Expand Up @@ -982,7 +982,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Argument TestType.fieldWithDefaultArgumentValue(input:) of type String has invalid value WRONG_TYPE.',
'Argument TestType.fieldWithDefaultArgumentValue(input:) has invalid value: String cannot represent a non string value: WRONG_TYPE',
locations: [{ line: 3, column: 48 }],
path: ['fieldWithDefaultArgumentValue'],
},
Expand Down Expand Up @@ -1022,7 +1022,7 @@ describe('Execute: Handles inputs', () => {

function invalidValueError(value: number, index: number) {
return {
message: `Variable "$input" got invalid value ${value} at "input[${index}]"; String cannot represent a non string value: ${value}`,
message: `Variable "$input" has invalid value at [${index}]: String cannot represent a non string value: ${value}`,
locations: [{ line: 2, column: 14 }],
};
}
Expand Down
144 changes: 59 additions & 85 deletions src/execution/values.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap';
import type { Maybe } from '../jsutils/Maybe';
import { keyMap } from '../jsutils/keyMap';
import { inspect } from '../jsutils/inspect';
import { invariant } from '../jsutils/invariant';
import { printPathArray } from '../jsutils/printPathArray';

import { GraphQLError } from '../error/GraphQLError';
Expand All @@ -17,14 +17,22 @@ import { print } from '../language/printer';
import type { GraphQLSchema } from '../type/schema';
import type { GraphQLInputType, GraphQLField } from '../type/definition';
import type { GraphQLDirective } from '../type/directives';
import { isInputType, isNonNullType } from '../type/definition';
import {
isInputType,
isNonNullType,
isRequiredArgument,
} from '../type/definition';

import { typeFromAST } from '../utilities/typeFromAST';
import {
coerceInputValue,
coerceInputLiteral,
coerceDefaultValue,
} from '../utilities/coerceInputValue';
import {
validateInputValue,
validateInputLiteral,
} from '../utilities/validateInputValue';

export interface VariableValues {
readonly sources: ReadOnlyObjMap<VariableValueSource>;
Expand Down Expand Up @@ -109,59 +117,38 @@ function coerceVariableValues(
continue;
}

if (!hasOwnProperty(inputs, varName)) {
const defaultValue = varDefNode.defaultValue;
if (defaultValue) {
sources[varName] = {
variable: varDefNode,
type: varType,
value: undefined,
};
coerced[varName] = coerceInputLiteral(defaultValue, varType);
} else if (isNonNullType(varType)) {
onError(
new GraphQLError(
`Variable "$${varName}" of required type ${varType} was not provided.`,
varDefNode,
),
);
}
continue;
}
const value = hasOwnProperty(inputs, varName) ? inputs[varName] : undefined;
sources[varName] = { variable: varDefNode, type: varType, value };

const value = inputs[varName];
if (value === null && isNonNullType(varType)) {
onError(
new GraphQLError(
`Variable "$${varName}" of non-null type ${varType} must not be null.`,
varDefNode,
),
);
continue;
if (value === undefined) {
if (varDefNode.defaultValue) {
coerced[varName] = coerceInputLiteral(varDefNode.defaultValue, varType);
continue;
} else if (!isNonNullType(varType)) {
// Non-provided values for nullable variables are omitted.
continue;
}
}

sources[varName] = { variable: varDefNode, type: varType, value };
coerced[varName] = coerceInputValue(
value,
varType,
(path, invalidValue, error) => {
let prefix =
`Variable "$${varName}" got invalid value ` + inspect(invalidValue);
if (path.length > 0) {
prefix += ` at "${varName}${printPathArray(path)}"`;
}
const coercedValue = coerceInputValue(value, varType);
if (coercedValue !== undefined) {
coerced[varName] = coercedValue;
} else {
validateInputValue(value, varType, (error, path) => {
onError(
new GraphQLError(
prefix + '; ' + error.message,
`Variable "$${varName}" has invalid value${printPathArray(path)}: ${
error.message
}`,
varDefNode,
undefined,
undefined,
undefined,
error.originalError,
),
);
},
);
});
}
}

return { sources, coerced };
Expand Down Expand Up @@ -193,65 +180,52 @@ export function getArgumentValues(
const argType = argDef.type;
const argumentNode = argNodeMap[name];

if (!argumentNode) {
if (!argumentNode && isRequiredArgument(argDef)) {
// Note: ProvidedRequiredArgumentsRule validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new GraphQLError(
`Argument ${argDef} of required type ${argType} was not provided.`,
node,
);
}

// Variables without a value are treated as if no argument was provided if
// the argument is not required.
if (
!argumentNode ||
(argumentNode.value.kind === Kind.VARIABLE &&
variableValues?.coerced[argumentNode.value.name.value] === undefined &&
!isRequiredArgument(argDef))
) {
if (argDef.defaultValue) {
coercedValues[name] = coerceDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument ${argDef} of required type ${argType} was not provided.`,
node,
);
}
continue;
}

const valueNode = argumentNode.value;
let isNull = valueNode.kind === Kind.NULL;

if (valueNode.kind === Kind.VARIABLE) {
const variableName = valueNode.name.value;
if (
variableValues == null ||
variableValues.coerced[variableName] === undefined
) {
if (argDef.defaultValue) {
coercedValues[name] = coerceDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument ${argDef} of required type ${argType} ` +
`was provided the variable "$${variableName}" which was not provided a runtime value.`,
valueNode,
);
}
continue;
}
isNull = variableValues.coerced[variableName] == null;
}

if (isNull && isNonNullType(argType)) {
throw new GraphQLError(
`Argument ${argDef} of non-null type ${argType} must not be null.`,
valueNode,
);
}

const coercedValue = coerceInputLiteral(valueNode, argType, variableValues);
if (coercedValue === undefined) {
// Note: ValuesOfCorrectTypeRule validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new GraphQLError(
`Argument ${argDef} of type ${argType} has invalid value ${print(
valueNode,
)}.`,
validateInputLiteral(
valueNode,
argType,
variableValues,
(error, path) => {
error.message = `Argument ${argDef} has invalid value${printPathArray(
path,
)}: ${error.message}`;
throw error;
},
);
// istanbul ignore next (validateInputLiteral should throw)
invariant(false, 'Invalid argument');
}
coercedValues[name] = coercedValue;
}
Expand Down
Loading

0 comments on commit 757931d

Please sign in to comment.