mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 15:59:24 +03:00
Enforce input coercion rules
This commit is contained in:
parent
f672f0c90c
commit
439959b292
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $values;
|
|
||||||
|
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 $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 ]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (NullValue::getNullValue() === $value) {
|
} else {
|
||||||
$value = null;
|
$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 ]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
$result[$name] = $value;
|
$coercedValues[$name] = $coercedValue;
|
||||||
}
|
}
|
||||||
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, $type->getWrappedType());
|
||||||
}
|
|
||||||
return self::isValidPHPValue($value, $ofType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
if (null !== $fieldValue) {
|
continue;
|
||||||
$obj[$fieldName] = $fieldValue;
|
|
||||||
}
|
}
|
||||||
|
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
|
||||||
|
if ($fieldValue === $undefined) {
|
||||||
|
// Intentionally return no value.
|
||||||
|
return $undefined;
|
||||||
}
|
}
|
||||||
return $obj;
|
$coercedObj[$fieldName] = $fieldValue;
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
$result[] = $value;
|
$coercedValues[] = null;
|
||||||
|
} else {
|
||||||
|
$itemValue = self::valueFromAST($itemAST, $itemType, $variables);
|
||||||
|
if ($undefined === $itemValue) {
|
||||||
|
// Invalid: intentionally return no value.
|
||||||
|
return $undefined;
|
||||||
}
|
}
|
||||||
return $result;
|
$coercedValues[] = $itemValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $coercedValues;
|
||||||
|
}
|
||||||
|
$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;
|
||||||
}
|
}
|
||||||
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
|
|
||||||
$values = [];
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
$fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null;
|
|
||||||
$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
|
$coercedObj = [];
|
||||||
if (null === $fieldValue && null === $field->defaultValue && !$field->defaultValueExists()) {
|
$fields = $type->getFields();
|
||||||
|
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
/** @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 ;
|
continue ;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Explicit null value or default value:
|
$fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables);
|
||||||
if (NullValue::getNullValue() === $fieldValue) {
|
|
||||||
$fieldValue = null;
|
|
||||||
} else if (null === $fieldValue) {
|
|
||||||
$fieldValue = $field->defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$values[$field->name] = $fieldValue;
|
if ($undefined === $fieldValue) {
|
||||||
|
// Invalid: intentionally return no value.
|
||||||
|
return $undefined;
|
||||||
}
|
}
|
||||||
return $values;
|
$coercedObj[$fieldName] = $fieldValue;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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($type->getWrappedType(), $valueAST);
|
||||||
}
|
|
||||||
return static::isValidLiteralValue($wrappedType, $valueAST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$valueAST || $valueAST instanceof NullValue) {
|
if (!$valueAST || $valueAST instanceof NullValue) {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 = [
|
||||||
|
'message' =>
|
||||||
'Variable "$input" expected value of type "TestType!" which cannot ' .
|
'Variable "$input" expected value of type "TestType!" which cannot ' .
|
||||||
'be used as an input type.',
|
'be used as an input type.',
|
||||||
[new SourceLocation(2, 17)]
|
'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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
244
tests/Utils/ValueFromAstTest.php
Normal file
244
tests/Utils/ValueFromAstTest.php
Normal 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 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user