Enforce input coercion rules

This commit is contained in:
vladar 2016-11-19 04:15:40 +07:00
parent f672f0c90c
commit 439959b292
11 changed files with 669 additions and 208 deletions

View File

@ -74,21 +74,36 @@ class Error extends \Exception implements \JsonSerializable
*/ */
public static function createLocatedError($error, $nodes = null, $path = null) public static function createLocatedError($error, $nodes = null, $path = null)
{ {
if ($error instanceof self) { if ($error instanceof self && $error->path) {
return $error; return $error;
} }
if ($error instanceof \Exception) { $source = $positions = $originalError = null;
if ($error instanceof self) {
$message = $error->getMessage(); $message = $error->getMessage();
$previous = $error; $originalError = $error;
$nodes = $error->nodes ?: $nodes;
$source = $error->source;
$positions = $error->positions;
} else if ($error instanceof \Exception) {
$message = $error->getMessage();
$originalError = $error;
} else { } else {
$message = (string) $error; $message = (string) $error;
$previous = null;
} }
return new static($message, $nodes, null, null, $path, $previous); return new static(
$message ?: 'An unknown error occurred.',
$nodes,
$source,
$positions,
$path,
$originalError
);
} }
/** /**
* @param Error $error * @param Error $error
* @return array * @return array

View File

@ -338,7 +338,7 @@ class Executor
: null; : null;
if ($skipAST) { if ($skipAST) {
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues); $argValues = Values::getArgumentValues($skipDirective, $skipAST, $exeContext->variableValues);
if (isset($argValues['if']) && $argValues['if'] === true) { if (isset($argValues['if']) && $argValues['if'] === true) {
return false; return false;
} }
@ -352,7 +352,7 @@ class Executor
: null; : null;
if ($includeAST) { if ($includeAST) {
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues); $argValues = Values::getArgumentValues($includeDirective, $includeAST, $exeContext->variableValues);
if (isset($argValues['if']) && $argValues['if'] === false) { if (isset($argValues['if']) && $argValues['if'] === false) {
return false; return false;
} }
@ -410,14 +410,6 @@ class Executor
$returnType = $fieldDef->getType(); $returnType = $fieldDef->getType();
// Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
$args = Values::getArgumentValues(
$fieldDef->args,
$fieldAST->arguments,
$exeContext->variableValues
);
// The resolve function's optional third argument is a collection of // The resolve function's optional third argument is a collection of
// information about the current execution state. // information about the current execution state.
$info = new ResolveInfo([ $info = new ResolveInfo([
@ -449,7 +441,15 @@ class Executor
// Get the resolve function, regardless of if its result is normal // Get the resolve function, regardless of if its result is normal
// or abrupt (error). // or abrupt (error).
$result = self::resolveOrError($resolveFn, $source, $args, $context, $info); $result = self::resolveOrError(
$exeContext,
$fieldDef,
$fieldAST,
$resolveFn,
$source,
$context,
$info
);
$result = self::completeValueCatchingError( $result = self::completeValueCatchingError(
$exeContext, $exeContext,
@ -463,11 +463,30 @@ class Executor
return $result; return $result;
} }
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` /**
// function. Returns the result of resolveFn or the abrupt-return Error object. * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
private static function resolveOrError($resolveFn, $source, $args, $context, $info) * function. Returns the result of resolveFn or the abrupt-return Error object.
*
* @param ExecutionContext $exeContext
* @param FieldDefinition $fieldDef
* @param Field $fieldAST
* @param callable $resolveFn
* @param mixed $source
* @param mixed $context
* @param ResolveInfo $info
* @return \Exception|mixed
*/
private static function resolveOrError($exeContext, $fieldDef, $fieldAST, $resolveFn, $source, $context, $info)
{ {
try { try {
// Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
$args = Values::getArgumentValues(
$fieldDef,
$fieldAST,
$exeContext->variableValues
);
return call_user_func($resolveFn, $source, $args, $context, $info); return call_user_func($resolveFn, $source, $args, $context, $info);
} catch (\Exception $error) { } catch (\Exception $error) {
return $error; return $error;
@ -783,8 +802,9 @@ class Executor
$i = 0; $i = 0;
$tmp = []; $tmp = [];
foreach ($result as $item) { foreach ($result as $item) {
$path[] = $i++; $fieldPath = $path;
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $path, $item); $fieldPath[] = $i++;
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $fieldPath, $item);
} }
return $tmp; return $tmp;
} }

View File

@ -5,11 +5,16 @@ namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\Argument; use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\LeafType;
@ -17,6 +22,7 @@ use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils; use GraphQL\Utils;
use GraphQL\Validator\DocumentValidator;
class Values class Values
{ {
@ -33,50 +39,126 @@ class Values
*/ */
public static function getVariableValues(Schema $schema, $definitionASTs, array $inputs) public static function getVariableValues(Schema $schema, $definitionASTs, array $inputs)
{ {
$values = []; $coercedValues = [];
foreach ($definitionASTs as $defAST) { foreach ($definitionASTs as $definitionAST) {
$varName = $defAST->variable->name->value; $varName = $definitionAST->variable->name->value;
$values[$varName] = self::getvariableValue($schema, $defAST, isset($inputs[$varName]) ? $inputs[$varName] : null); $varType = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type);
if (!Type::isInputType($varType)) {
throw new Error(
'Variable "$'.$varName.'" expected value of type ' .
'"' . Printer::doPrint($definitionAST->type) . '" which cannot be used as an input type.',
[$definitionAST->type]
);
}
if (!array_key_exists($varName, $inputs)) {
$defaultValue = $definitionAST->defaultValue;
if ($defaultValue) {
$coercedValues[$varName] = Utils\AST::valueFromAST($defaultValue, $varType);
}
if ($varType instanceof NonNull) {
throw new Error(
'Variable "$'.$varName .'" of required type ' .
'"'. Utils::printSafe($varType) . '" was not provided.',
[$definitionAST]
);
}
} else {
$value = $inputs[$varName];
$errors = self::isValidPHPValue($value, $varType);
if (!empty($errors)) {
$message = "\n" . implode("\n", $errors);
throw new Error(
'Variable "$' . $varName . '" got invalid value ' .
json_encode($value) . '.' . $message,
[$definitionAST]
);
}
$coercedValue = self::coerceValue($varType, $value);
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
$coercedValues[$varName] = $coercedValue;
}
} }
return $values; return $coercedValues;
} }
/** /**
* Prepares an object map of argument values given a list of argument * Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes. * definitions and list of argument AST nodes.
* *
* @param FieldArgument[] $argDefs * @param FieldDefinition|Directive $def
* @param Argument[] $argASTs * @param Field|\GraphQL\Language\AST\Directive $node
* @param $variableValues * @param $variableValues
* @return array * @return array
* @throws Error
*/ */
public static function getArgumentValues($argDefs, $argASTs, $variableValues) public static function getArgumentValues($def, $node, $variableValues)
{ {
if (!$argDefs) { $argDefs = $def->args;
$argASTs = $node->arguments;
if (!$argDefs || null === $argASTs) {
return []; return [];
} }
$argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) {
$coercedValues = [];
$undefined = Utils::undefined();
/** @var Argument[] $argASTMap */
$argASTMap = $argASTs ? Utils::keyMap($argASTs, function (Argument $arg) {
return $arg->name->value; return $arg->name->value;
}) : []; }) : [];
$result = [];
foreach ($argDefs as $argDef) { foreach ($argDefs as $argDef) {
$name = $argDef->name; $name = $argDef->name;
$valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null; $argType = $argDef->getType();
$value = Utils\AST::valueFromAST($valueAST, $argDef->getType(), $variableValues); $argumentAST = isset($argASTMap[$name]) ? $argASTMap[$name] : null;
if (null === $value && null === $argDef->defaultValue && !$argDef->defaultValueExists()) { if (!$argumentAST) {
continue; if ($argDef->defaultValueExists()) {
} $coercedValues[$name] = $argDef->defaultValue;
} else if ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
[$node]
);
}
} else if ($argumentAST->value instanceof Variable) {
$variableName = $argumentAST->value->name->value;
if (null === $value) { if ($variableValues && array_key_exists($variableName, $variableValues)) {
$value = $argDef->defaultValue; // Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName];
} else if ($argDef->defaultValueExists()) {
$coercedValues[$name] = $argDef->defaultValue;
} else if ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.',
[ $argumentAST->value ]
);
}
} else {
$valueAST = $argumentAST->value;
$coercedValue = Utils\AST::valueFromAST($valueAST, $argType, $variableValues);
if ($coercedValue === $undefined) {
$errors = DocumentValidator::isValidLiteralValue($argType, $valueAST);
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
throw new Error(
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueAST) . '.' . $message,
[ $argumentAST->value ]
);
}
$coercedValues[$name] = $coercedValue;
} }
if (NullValue::getNullValue() === $value) {
$value = null;
}
$result[$name] = $value;
} }
return $result; return $coercedValues;
} }
/** /**
@ -92,56 +174,6 @@ class Values
return Utils\AST::valueFromAST($valueAST, $type, $variables); return Utils\AST::valueFromAST($valueAST, $type, $variables);
} }
/**
* Given a variable definition, and any value of input, return a value which
* adheres to the variable definition, or throw an error.
*/
private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input)
{
$type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type);
$variable = $definitionAST->variable;
if (!$type || !Type::isInputType($type)) {
$printed = Printer::doPrint($definitionAST->type);
throw new Error(
"Variable \"\${$variable->name->value}\" expected value of type " .
"\"$printed\" which cannot be used as an input type.",
[ $definitionAST ]
);
}
$inputType = $type;
$errors = self::isValidPHPValue($input, $inputType);
if (empty($errors)) {
if (null === $input) {
$defaultValue = $definitionAST->defaultValue;
if ($defaultValue) {
$value = Utils\AST::valueFromAST($defaultValue, $inputType);
return $value === NullValue::getNullValue() ? null : $value;
}
}
return self::coerceValue($inputType, $input);
}
if (null === $input) {
$printed = Printer::doPrint($definitionAST->type);
throw new Error(
"Variable \"\${$variable->name->value}\" of required type " .
"\"$printed\" was not provided.",
[ $definitionAST ]
);
}
$message = $errors ? "\n" . implode("\n", $errors) : '';
$val = json_encode($input);
throw new Error(
"Variable \"\${$variable->name->value}\" got invalid value ".
"{$val}.{$message}",
[ $definitionAST ]
);
}
/** /**
* Given a PHP value and a GraphQL type, determine if the value will be * Given a PHP value and a GraphQL type, determine if the value will be
* accepted for that type. This is primarily useful for validating the * accepted for that type. This is primarily useful for validating the
@ -155,14 +187,10 @@ class Values
{ {
// A value must be provided if the type is non-null. // A value must be provided if the type is non-null.
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
$ofType = $type->getWrappedType();
if (null === $value) { if (null === $value) {
if ($ofType->name) { return ['Expected "' . Utils::printSafe($type) . '", found null.'];
return [ "Expected \"{$ofType->name}!\", found null." ];
}
return [ 'Expected non-null value, found null.' ];
} }
return self::isValidPHPValue($value, $ofType); return self::isValidPHPValue($value, $type->getWrappedType());
} }
if (null === $value) { if (null === $value) {
@ -235,9 +263,16 @@ class Values
*/ */
private static function coerceValue(Type $type, $value) private static function coerceValue(Type $type, $value)
{ {
$undefined = Utils::undefined();
if ($value === $undefined) {
return $undefined;
}
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
// Note: we're not checking that the result of coerceValue is non-null. if ($value === null) {
// We only call this function after calling isValidPHPValue. // Intentionally return no value.
return $undefined;
}
return self::coerceValue($type->getWrappedType(), $value); return self::coerceValue($type->getWrappedType(), $value);
} }
@ -248,32 +283,57 @@ class Values
if ($type instanceof ListOfType) { if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) { if (is_array($value) || $value instanceof \Traversable) {
return Utils::map($value, function($item) use ($itemType) { $coercedValues = [];
return Values::coerceValue($itemType, $item); foreach ($value as $item) {
}); $itemValue = self::coerceValue($itemType, $item);
if ($undefined === $itemValue) {
// Intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
return $coercedValues;
} else { } else {
return [self::coerceValue($itemType, $value)]; $coercedValue = self::coerceValue($itemType, $value);
if ($coercedValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
return [$coercedValue];
} }
} }
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$coercedObj = [];
$fields = $type->getFields(); $fields = $type->getFields();
$obj = [];
foreach ($fields as $fieldName => $field) { foreach ($fields as $fieldName => $field) {
$fieldValue = self::coerceValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null); if (!array_key_exists($fieldName, $value)) {
if (null === $fieldValue) { if ($field->defaultValueExists()) {
$fieldValue = $field->defaultValue; $coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Intentionally return no value.
return $undefined;
}
continue;
} }
if (null !== $fieldValue) { $fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
$obj[$fieldName] = $fieldValue; if ($fieldValue === $undefined) {
// Intentionally return no value.
return $undefined;
} }
$coercedObj[$fieldName] = $fieldValue;
} }
return $obj; return $coercedObj;
} }
if ($type instanceof LeafType) { if ($type instanceof LeafType) {
return $type->parseValue($value); $parsed = $type->parseValue($value);
if (null === $parsed) {
// null or invalid values represent a failure to parse correctly,
// in which case no value is returned.
return $undefined;
}
return $parsed;
} }
throw new InvariantViolation('Must be input type'); throw new InvariantViolation('Must be input type');

View File

@ -4,10 +4,4 @@ namespace GraphQL\Language\AST;
class NullValue extends Node implements Value class NullValue extends Node implements Value
{ {
public $kind = NodeType::NULL; public $kind = NodeType::NULL;
public static function getNullValue()
{
static $nullValue;
return $nullValue ?: $nullValue = new \stdClass();
}
} }

View File

@ -9,6 +9,12 @@ use \Traversable, \InvalidArgumentException;
class Utils class Utils
{ {
public static function undefined()
{
static $undefined;
return $undefined ?: $undefined = new \stdClass();
}
/** /**
* @param object $obj * @param object $obj
* @param array $vars * @param array $vars
@ -231,11 +237,7 @@ class Utils
public static function printSafe($var) public static function printSafe($var)
{ {
if ($var instanceof Type) { if ($var instanceof Type) {
// FIXME: Replace with schema printer call return $var->toString();
if ($var instanceof WrappingType) {
$var = $var->getWrappedType(true);
}
return $var->name;
} }
if (is_object($var)) { if (is_object($var)) {
return 'instance of ' . get_class($var); return 'instance of ' . get_class($var);

View File

@ -4,6 +4,7 @@ namespace GraphQL\Utils;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\BooleanValue; use GraphQL\Language\AST\BooleanValue;
use GraphQL\Language\AST\EnumValue; use GraphQL\Language\AST\EnumValue;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FloatValue; use GraphQL\Language\AST\FloatValue;
use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\ListValue;
@ -12,6 +13,7 @@ use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable; use GraphQL\Language\AST\Variable;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\IDType; use GraphQL\Type\Definition\IDType;
@ -178,6 +180,9 @@ class AST
* A GraphQL type must be provided, which will be used to interpret different * A GraphQL type must be provided, which will be used to interpret different
* GraphQL Value literals. * GraphQL Value literals.
* *
* Returns `null` when the value could not be validly coerced according to
* the provided type.
*
* | GraphQL Value | PHP Value | * | GraphQL Value | PHP Value |
* | -------------------- | ------------- | * | -------------------- | ------------- |
* | Input Object | Assoc Array | * | Input Object | Assoc Array |
@ -186,7 +191,7 @@ class AST
* | String | String | * | String | String |
* | Int / Float | Int / Float | * | Int / Float | Int / Float |
* | Enum Value | Mixed | * | Enum Value | Mixed |
* | Null Value | null | * | Null Value | stdClass | instance of NullValue::getNullValue()
* *
* @param $valueAST * @param $valueAST
* @param InputType $type * @param InputType $type
@ -196,30 +201,33 @@ class AST
*/ */
public static function valueFromAST($valueAST, InputType $type, $variables = null) public static function valueFromAST($valueAST, InputType $type, $variables = null)
{ {
if ($type instanceof NonNull) { $undefined = Utils::undefined();
// Note: we're not checking that the result of valueFromAST is non-null.
// We're assuming that this query has been validated and the value used
// here is of the correct type.
return self::valueFromAST($valueAST, $type->getWrappedType(), $variables);
}
if (!$valueAST) { if (!$valueAST) {
// When there is no AST, then there is also no value. // When there is no AST, then there is also no value.
// Importantly, this is different from returning the GraphQL null value. // Importantly, this is different from returning the GraphQL null value.
return ; return $undefined;
}
if ($type instanceof NonNull) {
if ($valueAST instanceof NullValue) {
// Invalid: intentionally return no value.
return $undefined;
}
return self::valueFromAST($valueAST, $type->getWrappedType(), $variables);
} }
if ($valueAST instanceof NullValue) { if ($valueAST instanceof NullValue) {
// This is explicitly returning the value null. // This is explicitly returning the value null.
return NullValue::getNullValue(); return null;
} }
if ($valueAST instanceof Variable) { if ($valueAST instanceof Variable) {
$variableName = $valueAST->name->value; $variableName = $valueAST->name->value;
if (!$variables || !isset($variables[$variableName])) { if (!$variables || !array_key_exists($variableName, $variables)) {
// No valid return value. // No valid return value.
return ; return $undefined;
} }
// Note: we're not doing any checking that this variable is correct. We're // Note: we're not doing any checking that this variable is correct. We're
// assuming that this query has been validated and the variable usage here // assuming that this query has been validated and the variable usage here
@ -229,51 +237,99 @@ class AST
if ($type instanceof ListOfType) { if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
$items = $valueAST instanceof ListValue ? $valueAST->values : [$valueAST];
$result = []; if ($valueAST instanceof ListValue) {
foreach ($items as $itemAST) { $coercedValues = [];
$value = self::valueFromAST($itemAST, $itemType, $variables); $itemASTs = $valueAST->values;
if ($value === NullValue::getNullValue()) { foreach ($itemASTs as $itemAST) {
$value = null; if (self::isMissingVariable($itemAST, $variables)) {
// If an array contains a missing variable, it is either coerced to
// null or if the item type is non-null, it considered invalid.
if ($itemType instanceof NonNull) {
// Invalid: intentionally return no value.
return $undefined;
}
$coercedValues[] = null;
} else {
$itemValue = self::valueFromAST($itemAST, $itemType, $variables);
if ($undefined === $itemValue) {
// Invalid: intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
} }
$result[] = $value; return $coercedValues;
} }
return $result; $coercedValue = self::valueFromAST($valueAST, $itemType, $variables);
if ($undefined === $coercedValue) {
// Invalid: intentionally return no value.
return $undefined;
}
return [$coercedValue];
} }
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if (!$valueAST instanceof ObjectValue) { if (!$valueAST instanceof ObjectValue) {
// No valid return value. // Invalid: intentionally return no value.
return ; return $undefined;
} }
$coercedObj = [];
$fields = $type->getFields();
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;}); $fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
$values = [];
foreach ($fields as $field) { foreach ($fields as $field) {
$fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null; /** @var Value $fieldAST */
$fieldName = $field->name;
$fieldAST = isset($fieldASTs[$fieldName]) ? $fieldASTs[$fieldName] : null;
if (!$fieldAST || self::isMissingVariable($fieldAST->value, $variables)) {
if ($field->defaultValueExists()) {
$coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Invalid: intentionally return no value.
return $undefined;
}
continue ;
}
$fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables); $fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables);
// If field is not in AST and defaultValue was never set for this field - do not include it in result if ($undefined === $fieldValue) {
if (null === $fieldValue && null === $field->defaultValue && !$field->defaultValueExists()) { // Invalid: intentionally return no value.
continue; return $undefined;
} }
$coercedObj[$fieldName] = $fieldValue;
// Set Explicit null value or default value:
if (NullValue::getNullValue() === $fieldValue) {
$fieldValue = null;
} else if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
$values[$field->name] = $fieldValue;
} }
return $values; return $coercedObj;
} }
if ($type instanceof LeafType) { if ($type instanceof LeafType) {
return $type->parseLiteral($valueAST); $parsed = $type->parseLiteral($valueAST);
if (null === $parsed) {
// null represent a failure to parse correctly,
// in which case no value is returned.
return $undefined;
}
return $parsed;
} }
throw new InvariantViolation('Must be input type'); throw new InvariantViolation('Must be input type');
} }
/**
* Returns true if the provided valueAST is a variable which is not defined
* in the set of variables.
* @param $valueAST
* @param $variables
* @return bool
*/
private static function isMissingVariable($valueAST, $variables)
{
return $valueAST instanceof Variable &&
(!$variables || !array_key_exists($valueAST->name->value, $variables));
}
} }

View File

@ -157,14 +157,10 @@ class DocumentValidator
{ {
// A value must be provided if the type is non-null. // A value must be provided if the type is non-null.
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
$wrappedType = $type->getWrappedType();
if (!$valueAST || $valueAST instanceof NullValue) { if (!$valueAST || $valueAST instanceof NullValue) {
if ($wrappedType->name) { return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ];
return [ "Expected \"{$wrappedType->name}!\", found null." ];
}
return ['Expected non-null value, found null.'];
} }
return static::isValidLiteralValue($wrappedType, $valueAST); return static::isValidLiteralValue($type->getWrappedType(), $valueAST);
} }
if (!$valueAST || $valueAST instanceof NullValue) { if (!$valueAST || $valueAST instanceof NullValue) {

View File

@ -199,7 +199,7 @@ class QueryComplexity extends AbstractQuerySecurity
$this->variableDefs, $this->variableDefs,
$rawVariableValues $rawVariableValues
); );
$args = Values::getArgumentValues($fieldDef->args, $node->arguments, $variableValues); $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
} }
return $args; return $args;

View File

@ -251,12 +251,11 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
null null
] ]
], ],
'errors' => [ 'errors' => [[
FormattedError::create( 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'Runtime Object type "Human" is not a possible type for "Pet".', 'locations' => [['line' => 2, 'column' => 11]],
[new SourceLocation(2, 11)] 'path' => ['pets', 2]
) ]]
]
]; ];
$actual = GraphQL::execute($schema, $query); $actual = GraphQL::execute($schema, $query);
@ -349,12 +348,11 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
null null
] ]
], ],
'errors' => [ 'errors' => [[
FormattedError::create( 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'Runtime Object type "Human" is not a possible type for "Pet".', 'locations' => [['line' => 2, 'column' => 11]],
[new SourceLocation(2, 11)] 'path' => ['pets', 2]
) ]]
]
]; ];
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
} }

View File

@ -81,19 +81,24 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$result = Executor::execute($this->schema(), $ast)->toArray(); $result = Executor::execute($this->schema(), $ast)->toArray();
$expected = [ $expected = [
'data' => ['fieldWithObjectInput' => null] 'data' => ['fieldWithObjectInput' => null],
'errors' => [[
'message' => 'Argument "input" got invalid value ["foo", "bar", "baz"].' . "\n" .
'Expected "TestInputObject", found not an object.',
'path' => ['fieldWithObjectInput']
]]
]; ];
$this->assertEquals($expected, $result); $this->assertArraySubset($expected, $result);
// properly runs parseLiteral on complex scalar types // properly runs parseLiteral on complex scalar types
$doc = ' $doc = '
{ {
fieldWithObjectInput(input: {a: "foo", d: "SerializedValue"}) fieldWithObjectInput(input: {c: "foo", d: "SerializedValue"})
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$this->assertEquals( $this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","d":"DeserializedValue"}']], ['data' => ['fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}']],
Executor::execute($this->schema(), $ast)->toArray() Executor::execute($this->schema(), $ast)->toArray()
); );
} }
@ -332,6 +337,24 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
// Describe: Handles non-nullable scalars // Describe: Handles non-nullable scalars
/**
* @it allows non-nullable inputs to be omitted given a default
*/
public function testAllowsNonNullableInputsToBeOmittedGivenADefault()
{
$doc = '
query SetsNonNullable($value: String = "default") {
fieldWithNonNullableStringInput(input: $value)
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => ['fieldWithNonNullableStringInput' => '"default"']
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/** /**
* @it does not allow non-nullable inputs to be omitted in a variable * @it does not allow non-nullable inputs to be omitted in a variable
*/ */
@ -368,13 +391,15 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
try { try {
Executor::execute($this->schema(), $ast, null, ['value' => null]); Executor::execute($this->schema(), $ast, null, null, ['value' => null]);
$this->fail('Expected exception not thrown'); $this->fail('Expected exception not thrown');
} catch (Error $e) { } catch (Error $e) {
$expected = FormattedError::create( $expected = [
'Variable "$value" of required type "String!" was not provided.', 'message' =>
[new SourceLocation(2, 31)] 'Variable "$value" got invalid value null.' . "\n".
); 'Expected "String!", found null.',
'locations' => [['line' => 2, 'column' => 31]]
];
$this->assertEquals($expected, Error::formatError($e)); $this->assertEquals($expected, Error::formatError($e));
} }
} }
@ -411,9 +436,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @it passes along null for non-nullable inputs if explcitly set in the query * @it reports error for missing non-nullable inputs
*/ */
public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery() public function testReportsErrorForMissingNonNullableInputs()
{ {
$doc = ' $doc = '
{ {
@ -421,7 +446,44 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNonNullableStringInput' => null]]; $expected = [
'data' => ['fieldWithNonNullableStringInput' => null],
'errors' => [[
'message' => 'Argument "input" of required type "String!" was not provided.',
'locations' => [ [ 'line' => 3, 'column' => 9 ] ],
'path' => [ 'fieldWithNonNullableStringInput' ]
]]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/**
* @it reports error for non-provided variables for non-nullable inputs
*/
public function testReportsErrorForNonProvidedVariablesForNonNullableInputs()
{
// Note: this test would typically fail validation before encountering
// this execution error, however for queries which previously validated
// and are being run against a new schema which have introduced a breaking
// change to make a formerly non-required argument required, this asserts
// failure before allowing the underlying code to receive a non-null value.
$doc = '
{
fieldWithNonNullableStringInput(input: $foo)
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => ['fieldWithNonNullableStringInput' => null],
'errors' => [[
'message' =>
'Argument "input" of required type "String!" was provided the ' .
'variable "$foo" which was not provided a runtime value.',
'locations' => [['line' => 3, 'column' => 48]],
'path' => ['fieldWithNonNullableStringInput']
]]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
@ -485,7 +547,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = FormattedError::create( $expected = FormattedError::create(
'Variable "$input" of required type "[String]!" was not provided.', 'Variable "$input" got invalid value null.' . "\n" .
'Expected "[String]!", found null.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
); );
@ -596,7 +659,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = FormattedError::create( $expected = FormattedError::create(
'Variable "$input" of required type "[String!]!" was not provided.', 'Variable "$input" got invalid value null.' . "\n" .
'Expected "[String!]!", found null.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
); );
try { try {
@ -663,11 +727,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
Executor::execute($this->schema(), $ast, null, null, $vars); Executor::execute($this->schema(), $ast, null, null, $vars);
$this->fail('Expected exception not thrown'); $this->fail('Expected exception not thrown');
} catch (Error $error) { } catch (Error $error) {
$expected = FormattedError::create( $expected = [
'Variable "$input" expected value of type "TestType!" which cannot ' . 'message' =>
'be used as an input type.', 'Variable "$input" expected value of type "TestType!" which cannot ' .
[new SourceLocation(2, 17)] 'be used as an input type.',
); 'locations' => [['line' => 2, 'column' => 25]]
];
$this->assertEquals($expected, Error::formatError($error)); $this->assertEquals($expected, Error::formatError($error));
} }
} }
@ -692,7 +757,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$expected = FormattedError::create( $expected = FormattedError::create(
'Variable "$input" expected value of type "UnknownType!" which ' . 'Variable "$input" expected value of type "UnknownType!" which ' .
'cannot be used as an input type.', 'cannot be used as an input type.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 25)]
); );
$this->assertEquals($expected, Error::formatError($error)); $this->assertEquals($expected, Error::formatError($error));
} }
@ -715,9 +780,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @it when nullable variable provided * @it when omitted variable provided
*/ */
public function testWhenNullableVariableProvided() public function testWhenOmittedVariableProvided()
{ {
$ast = Parser::parse('query optionalVariable($optional: String) { $ast = Parser::parse('query optionalVariable($optional: String) {
fieldWithDefaultArgumentValue(input: $optional) fieldWithDefaultArgumentValue(input: $optional)
@ -730,16 +795,27 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @it when argument provided cannot be parsed * @it not when argument cannot be coerced
*/ */
public function testWhenArgumentProvidedCannotBeParsed() public function testNotWhenArgumentCannotBeCoerced()
{ {
$ast = Parser::parse('{ $ast = Parser::parse('{
fieldWithDefaultArgumentValue(input: WRONG_TYPE) fieldWithDefaultArgumentValue(input: WRONG_TYPE)
}'); }');
$expected = [
'data' => ['fieldWithDefaultArgumentValue' => null],
'errors' => [[
'message' =>
'Argument "input" got invalid value WRONG_TYPE.' . "\n" .
'Expected type "String", found WRONG_TYPE.',
'locations' => [ [ 'line' => 2, 'column' => 50 ] ],
'path' => [ 'fieldWithDefaultArgumentValue' ]
]]
];
$this->assertEquals( $this->assertEquals(
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']], $expected,
Executor::execute($this->schema(), $ast)->toArray() Executor::execute($this->schema(), $ast)->toArray()
); );
} }

View File

@ -0,0 +1,244 @@
<?php
namespace GraphQL\Tests\Utils;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\Parser;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
use GraphQL\Utils\AST;
class ValueFromAstTest extends \PHPUnit_Framework_TestCase
{
private function runTestCase($type, $valueText, $expected)
{
$this->assertEquals($expected, AST::valueFromAST(Parser::parseValue($valueText), $type));
}
private function runTestCaseWithVars($variables, $type, $valueText, $expected)
{
$this->assertEquals($expected, AST::valueFromAST(Parser::parseValue($valueText), $type, $variables));
}
/**
* @it rejects empty input
*/
public function testRejectsEmptyInput()
{
$this->assertEquals(Utils::undefined(), AST::valueFromAST(null, Type::boolean()));
}
/**
* @it converts according to input coercion rules
*/
public function testConvertsAccordingToInputCoercionRules()
{
$this->runTestCase(Type::boolean(), 'true', true);
$this->runTestCase(Type::boolean(), 'false', false);
$this->runTestCase(Type::int(), '123', 123);
$this->runTestCase(Type::float(), '123', 123);
$this->runTestCase(Type::float(), '123.456', 123.456);
$this->runTestCase(Type::string(), '"abc123"', 'abc123');
$this->runTestCase(Type::id(), '123456', '123456');
$this->runTestCase(Type::id(), '"123456"', '123456');
}
/**
* @it does not convert when input coercion rules reject a value
*/
public function testDoesNotConvertWhenInputCoercionRulesRejectAValue()
{
$undefined = Utils::undefined();
$this->runTestCase(Type::boolean(), '123', $undefined);
$this->runTestCase(Type::int(), '123.456', $undefined);
$this->runTestCase(Type::int(), 'true', $undefined);
$this->runTestCase(Type::int(), '"123"', $undefined);
$this->runTestCase(Type::float(), '"123"', $undefined);
$this->runTestCase(Type::string(), '123', $undefined);
$this->runTestCase(Type::string(), 'true', $undefined);
$this->runTestCase(Type::id(), '123.456', $undefined);
}
/**
* @it converts enum values according to input coercion rules
*/
public function testConvertsEnumValuesAccordingToInputCoercionRules()
{
$testEnum = new EnumType([
'name' => 'TestColor',
'values' => [
'RED' => ['value' => 1],
'GREEN' => ['value' => 2],
'BLUE' => ['value' => 3],
]
]);
$this->runTestCase($testEnum, 'RED', 1);
$this->runTestCase($testEnum, 'BLUE', 3);
$this->runTestCase($testEnum, '3', Utils::undefined());
$this->runTestCase($testEnum, '"BLUE"', Utils::undefined());
$this->runTestCase($testEnum, 'null', null);
}
/**
* @it coerces to null unless non-null
*/
public function testCoercesToNullUnlessNonNull()
{
$this->runTestCase(Type::boolean(), 'null', null);
$this->runTestCase(Type::nonNull(Type::boolean()), 'null', Utils::undefined());
}
/**
* @it coerces lists of values
*/
public function testCoercesListsOfValues()
{
$listOfBool = Type::listOf(Type::boolean());
$undefined = Utils::undefined();
$this->runTestCase($listOfBool, 'true', [ true ]);
$this->runTestCase($listOfBool, '123', $undefined);
$this->runTestCase($listOfBool, 'null', null);
$this->runTestCase($listOfBool, '[true, false]', [ true, false ]);
$this->runTestCase($listOfBool, '[true, 123]', $undefined);
$this->runTestCase($listOfBool, '[true, null]', [ true, null ]);
$this->runTestCase($listOfBool, '{ true: true }', $undefined);
}
/**
* @it coerces non-null lists of values
*/
public function testCoercesNonNullListsOfValues()
{
$nonNullListOfBool = Type::nonNull(Type::listOf(Type::boolean()));
$undefined = Utils::undefined();
$this->runTestCase($nonNullListOfBool, 'true', [ true ]);
$this->runTestCase($nonNullListOfBool, '123', $undefined);
$this->runTestCase($nonNullListOfBool, 'null', $undefined);
$this->runTestCase($nonNullListOfBool, '[true, false]', [ true, false ]);
$this->runTestCase($nonNullListOfBool, '[true, 123]', $undefined);
$this->runTestCase($nonNullListOfBool, '[true, null]', [ true, null ]);
}
/**
* @it coerces lists of non-null values
*/
public function testCoercesListsOfNonNullValues()
{
$listOfNonNullBool = Type::listOf(Type::nonNull(Type::boolean()));
$undefined = Utils::undefined();
$this->runTestCase($listOfNonNullBool, 'true', [ true ]);
$this->runTestCase($listOfNonNullBool, '123', $undefined);
$this->runTestCase($listOfNonNullBool, 'null', null);
$this->runTestCase($listOfNonNullBool, '[true, false]', [ true, false ]);
$this->runTestCase($listOfNonNullBool, '[true, 123]', $undefined);
$this->runTestCase($listOfNonNullBool, '[true, null]', $undefined);
}
/**
* @it coerces non-null lists of non-null values
*/
public function testCoercesNonNullListsOfNonNullValues()
{
$nonNullListOfNonNullBool = Type::nonNull(Type::listOf(Type::nonNull(Type::boolean())));
$undefined = Utils::undefined();
$this->runTestCase($nonNullListOfNonNullBool, 'true', [ true ]);
$this->runTestCase($nonNullListOfNonNullBool, '123', $undefined);
$this->runTestCase($nonNullListOfNonNullBool, 'null', $undefined);
$this->runTestCase($nonNullListOfNonNullBool, '[true, false]', [ true, false ]);
$this->runTestCase($nonNullListOfNonNullBool, '[true, 123]', $undefined);
$this->runTestCase($nonNullListOfNonNullBool, '[true, null]', $undefined);
}
private $inputObj;
private function inputObj()
{
return $this->inputObj ?: $this->inputObj = new InputObjectType([
'name' => 'TestInput',
'fields' => [
'int' => [ 'type' => Type::int(), 'defaultValue' => 42 ],
'bool' => [ 'type' => Type::boolean() ],
'requiredBool' => [ 'type' => Type::nonNull(Type::boolean()) ],
]
]);
}
/**
* @it coerces input objects according to input coercion rules
*/
public function testCoercesInputObjectsAccordingToInputCoercionRules()
{
$testInputObj = $this->inputObj();
$undefined = Utils::undefined();
$this->runTestCase($testInputObj, 'null', null);
$this->runTestCase($testInputObj, '123', $undefined);
$this->runTestCase($testInputObj, '[]', $undefined);
$this->runTestCase($testInputObj, '{ int: 123, requiredBool: false }', ['int' => 123, 'requiredBool' => false]);
$this->runTestCase($testInputObj, '{ bool: true, requiredBool: false }', [ 'int' => 42, 'bool' => true, 'requiredBool' => false ]);
$this->runTestCase($testInputObj, '{ int: true, requiredBool: true }', $undefined);
$this->runTestCase($testInputObj, '{ requiredBool: null }', $undefined);
$this->runTestCase($testInputObj, '{ bool: true }', $undefined);
}
/**
* @it accepts variable values assuming already coerced
*/
public function testAcceptsVariableValuesAssumingAlreadyCoerced()
{
$this->runTestCaseWithVars([], Type::boolean(), '$var', Utils::undefined());
$this->runTestCaseWithVars([ 'var' => true ], Type::boolean(), '$var', true);
$this->runTestCaseWithVars([ 'var' => null ], Type::boolean(), '$var', null);
}
/**
* @it asserts variables are provided as items in lists
*/
public function testAssertsVariablesAreProvidedAsItemsInLists()
{
$listOfBool = Type::listOf(Type::boolean());
$listOfNonNullBool = Type::listOf(Type::nonNull(Type::boolean()));
$this->runTestCaseWithVars([], $listOfBool, '[ $foo ]', [ null ]);
$this->runTestCaseWithVars([], $listOfNonNullBool, '[ $foo ]', Utils::undefined());
$this->runTestCaseWithVars([ 'foo' => true ], $listOfNonNullBool, '[ $foo ]', [ true ]);
// Note: variables are expected to have already been coerced, so we
// do not expect the singleton wrapping behavior for variables.
$this->runTestCaseWithVars([ 'foo' => true ], $listOfNonNullBool, '$foo', true);
$this->runTestCaseWithVars([ 'foo' => [ true ] ], $listOfNonNullBool, '$foo', [ true ]);
}
/**
* @it omits input object fields for unprovided variables
*/
public function testOmitsInputObjectFieldsForUnprovidedVariables()
{
$testInputObj = $this->inputObj();
$this->runTestCaseWithVars(
[],
$testInputObj,
'{ int: $foo, bool: $foo, requiredBool: true }',
[ 'int' => 42, 'requiredBool' => true ]
);
$this->runTestCaseWithVars(
[],
$testInputObj,
'{ requiredBool: $foo }',
Utils::undefined()
);
$this->runTestCaseWithVars(
[ 'foo' => true ],
$testInputObj,
'{ requiredBool: $foo }',
[ 'int' => 42, 'requiredBool' => true ]
);
}
}