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 May 14, 2021
1 parent d3aaf3e commit 026cdd2
Show file tree
Hide file tree
Showing 21 changed files with 1,516 additions and 657 deletions.
6 changes: 3 additions & 3 deletions src/execution/__tests__/nonnull-test.js
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 "cannotBeNull" of non-null type "String!" must not be null.',
'Argument "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 "cannotBeNull" of required type "String!" was provided the variable "$testVar" which was not provided a runtime value.',
'Argument "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 "cannotBeNull" of non-null type "String!" must not be null.',
'Argument "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
35 changes: 18 additions & 17 deletions src/execution/__tests__/variables-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Argument "input" has invalid value ["foo", "bar", "baz"].',
'Argument "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 @@ -368,7 +368,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 @@ -382,7 +382,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 @@ -396,7 +396,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 @@ -415,12 +415,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 @@ -437,7 +437,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 @@ -612,7 +612,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 @@ -631,7 +631,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 @@ -697,7 +697,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 @@ -725,7 +725,7 @@ describe('Execute: Handles inputs', () => {
errors: [
{
message:
'Argument "input" of required type "String!" was provided the variable "$foo" which was not provided a runtime value.',
'Argument "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 @@ -780,7 +780,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 @@ -843,7 +843,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 @@ -862,7 +862,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 @@ -892,7 +892,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 @@ -976,7 +976,8 @@ describe('Execute: Handles inputs', () => {
},
errors: [
{
message: 'Argument "input" has invalid value WRONG_TYPE.',
message:
'Argument "input" has invalid value: String cannot represent a non string value: WRONG_TYPE',
locations: [{ line: 3, column: 48 }],
path: ['fieldWithDefaultArgumentValue'],
},
Expand Down Expand Up @@ -1016,7 +1017,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
146 changes: 58 additions & 88 deletions src/execution/values.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from '../jsutils/ObjMap';
import { inspect } from '../jsutils/inspect';
import { hasOwnProperty } from '../jsutils/hasOwnProperty';
import { invariant } from '../jsutils/invariant';
import { keyMap } from '../jsutils/keyMap';
import { printPathArray } from '../jsutils/printPathArray';

Expand All @@ -16,12 +17,18 @@ 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,
isRequiredInput,
} from '../type/definition';
import { getCoercedDefaultValue } from '../type/defaultValues';

import { typeFromAST } from '../utilities/typeFromAST';
import { valueFromAST } from '../utilities/valueFromAST';
import { coerceInputValue } from '../utilities/coerceInputValue';
import { validateInputValue } from '../utilities/validateInputValue';
import { validateInputLiteral } from '../utilities/validateInputLiteral';

export type VariableValues = {|
+sources: ReadOnlyObjMap<{|
Expand Down Expand Up @@ -104,60 +111,38 @@ function coerceVariableValues(
continue;
}

if (!hasOwnProperty(inputs, varName)) {
const value = hasOwnProperty(inputs, varName) ? inputs[varName] : undefined;
sources[varName] = { variable: varDefNode, type: varType, value };

if (value === undefined) {
if (varDefNode.defaultValue) {
sources[varName] = {
variable: varDefNode,
type: varType,
value: undefined,
};
coerced[varName] = valueFromAST(varDefNode.defaultValue, varType);
} else if (isNonNullType(varType)) {
const varTypeStr = inspect(varType);
onError(
new GraphQLError(
`Variable "$${varName}" of required type "${varTypeStr}" was not provided.`,
varDefNode,
),
);
continue;
} else if (!isNonNullType(varType)) {
// Non-provided values for nullable variables are omitted.
continue;
}
continue;
}

const value = inputs[varName];
if (value === null && isNonNullType(varType)) {
const varTypeStr = inspect(varType);
onError(
new GraphQLError(
`Variable "$${varName}" of non-null type "${varTypeStr}" must not be null.`,
varDefNode,
),
);
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 @@ -189,65 +174,54 @@ export function getArgumentValues(
const argType = argDef.type;
const argumentNode = argNodeMap[name];

if (!argumentNode) {
if (!argumentNode && isRequiredInput(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 "${name}" of required type "${String(
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 &&
!isRequiredInput(argDef))
) {
if (argDef.defaultValue) {
coercedValues[name] = getCoercedDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument "${name}" of required type "${inspect(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] = getCoercedDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument "${name}" of required type "${inspect(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 "${name}" of non-null type "${inspect(argType)}" ` +
'must not be null.',
valueNode,
);
}

const coercedValue = valueFromAST(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 "${name}" has invalid value ${print(valueNode)}.`,
validateInputLiteral(
valueNode,
argType,
variableValues,
(error, path) => {
error.message = `Argument "${name}" has invalid value${printPathArray(
path,
)}: ${error.message}`;
throw error;
},
);
// istanbul ignore next (validateInputLiteral should throw)
invariant(false, 'Invalid argument');
}
coercedValues[name] = coercedValue;
}
Expand Down Expand Up @@ -279,7 +253,3 @@ export function getDirectiveValues(
return getArgumentValues(directiveDef, directiveNode, variableValues);
}
}

function hasOwnProperty(obj: mixed, prop: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
Loading

0 comments on commit 026cdd2

Please sign in to comment.