Version 4.5.0
This commit is contained in:
1 parent
1a0c7fd3c2
commit
f79dcf067a
3791 files changed
+248032
-76372
No files matched your search
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NullValueNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\LeafType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\Rules\AbstractValidationRule;
|
||||
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
|
||||
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
||||
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNames;
|
||||
use GraphQL\Validator\Rules\KnownDirectives;
|
||||
use GraphQL\Validator\Rules\KnownFragmentNames;
|
||||
use GraphQL\Validator\Rules\KnownTypeNames;
|
||||
use GraphQL\Validator\Rules\LoneAnonymousOperation;
|
||||
use GraphQL\Validator\Rules\NoFragmentCycles;
|
||||
use GraphQL\Validator\Rules\NoUndefinedVariables;
|
||||
use GraphQL\Validator\Rules\NoUnusedFragments;
|
||||
use GraphQL\Validator\Rules\NoUnusedVariables;
|
||||
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
||||
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
||||
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use GraphQL\Validator\Rules\ScalarLeafs;
|
||||
use GraphQL\Validator\Rules\UniqueArgumentNames;
|
||||
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
|
||||
use GraphQL\Validator\Rules\UniqueFragmentNames;
|
||||
use GraphQL\Validator\Rules\UniqueInputFieldNames;
|
||||
use GraphQL\Validator\Rules\UniqueOperationNames;
|
||||
use GraphQL\Validator\Rules\UniqueVariableNames;
|
||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||
|
||||
/**
|
||||
* Implements the "Validation" section of the spec.
|
||||
*
|
||||
* Validation runs synchronously, returning an array of encountered errors, or
|
||||
* an empty array if no errors were encountered and the document is valid.
|
||||
*
|
||||
* A list of specific validation rules may be provided. If not provided, the
|
||||
* default list of rules defined by the GraphQL specification will be used.
|
||||
*
|
||||
* Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule
|
||||
* which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)).
|
||||
*
|
||||
* Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
|
||||
* or array of such instances when invalid.
|
||||
*
|
||||
* Optionally a custom TypeInfo instance may be provided. If not provided, one
|
||||
* will be created from the provided schema.
|
||||
*/
|
||||
class DocumentValidator
|
||||
{
|
||||
private static $rules = [];
|
||||
|
||||
private static $defaultRules;
|
||||
|
||||
private static $securityRules;
|
||||
|
||||
private static $initRules = false;
|
||||
|
||||
/**
|
||||
* Primary method for query validation. See class description for details.
|
||||
*
|
||||
* @api
|
||||
* @param Schema $schema
|
||||
* @param DocumentNode $ast
|
||||
* @param AbstractValidationRule[]|null $rules
|
||||
* @param TypeInfo|null $typeInfo
|
||||
* @return Error[]
|
||||
*/
|
||||
public static function validate(
|
||||
Schema $schema,
|
||||
DocumentNode $ast,
|
||||
array $rules = null,
|
||||
TypeInfo $typeInfo = null
|
||||
)
|
||||
{
|
||||
if (null === $rules) {
|
||||
$rules = static::allRules();
|
||||
}
|
||||
$typeInfo = $typeInfo ?: new TypeInfo($schema);
|
||||
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules);
|
||||
return $errors;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all global validation rules.
|
||||
*
|
||||
* @api
|
||||
* @return AbstractValidationRule[]
|
||||
*/
|
||||
public static function allRules()
|
||||
{
|
||||
if (!self::$initRules) {
|
||||
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
|
||||
static::$initRules = true;
|
||||
}
|
||||
|
||||
return self::$rules;
|
||||
}
|
||||
|
||||
public static function defaultRules()
|
||||
{
|
||||
if (null === self::$defaultRules) {
|
||||
self::$defaultRules = [
|
||||
UniqueOperationNames::class => new UniqueOperationNames(),
|
||||
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
|
||||
KnownTypeNames::class => new KnownTypeNames(),
|
||||
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
|
||||
VariablesAreInputTypes::class => new VariablesAreInputTypes(),
|
||||
ScalarLeafs::class => new ScalarLeafs(),
|
||||
FieldsOnCorrectType::class => new FieldsOnCorrectType(),
|
||||
UniqueFragmentNames::class => new UniqueFragmentNames(),
|
||||
KnownFragmentNames::class => new KnownFragmentNames(),
|
||||
NoUnusedFragments::class => new NoUnusedFragments(),
|
||||
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
|
||||
NoFragmentCycles::class => new NoFragmentCycles(),
|
||||
UniqueVariableNames::class => new UniqueVariableNames(),
|
||||
NoUndefinedVariables::class => new NoUndefinedVariables(),
|
||||
NoUnusedVariables::class => new NoUnusedVariables(),
|
||||
KnownDirectives::class => new KnownDirectives(),
|
||||
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
|
||||
KnownArgumentNames::class => new KnownArgumentNames(),
|
||||
UniqueArgumentNames::class => new UniqueArgumentNames(),
|
||||
ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(),
|
||||
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
|
||||
DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(),
|
||||
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
|
||||
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
|
||||
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
|
||||
];
|
||||
}
|
||||
|
||||
return self::$defaultRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function securityRules()
|
||||
{
|
||||
// This way of defining rules is deprecated
|
||||
// When custom security rule is required - it should be just added via DocumentValidator::addRule();
|
||||
// TODO: deprecate this
|
||||
|
||||
if (null === self::$securityRules) {
|
||||
self::$securityRules = [
|
||||
DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
|
||||
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
|
||||
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
|
||||
];
|
||||
}
|
||||
return self::$securityRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns global validation rule by name. Standard rules are named by class name, so
|
||||
* example usage for such rules:
|
||||
*
|
||||
* $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class);
|
||||
*
|
||||
* @api
|
||||
* @param string $name
|
||||
* @return AbstractValidationRule
|
||||
*/
|
||||
public static function getRule($name)
|
||||
{
|
||||
$rules = static::allRules();
|
||||
|
||||
if (isset($rules[$name])) {
|
||||
return $rules[$name];
|
||||
}
|
||||
|
||||
$name = "GraphQL\\Validator\\Rules\\$name";
|
||||
return isset($rules[$name]) ? $rules[$name] : null ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rule to list of global validation rules
|
||||
*
|
||||
* @api
|
||||
* @param AbstractValidationRule $rule
|
||||
*/
|
||||
public static function addRule(AbstractValidationRule $rule)
|
||||
{
|
||||
self::$rules[$rule->getName()] = $rule;
|
||||
}
|
||||
|
||||
public static function isError($value)
|
||||
{
|
||||
return is_array($value)
|
||||
? count(array_filter($value, function($item) { return $item instanceof \Exception || $item instanceof \Throwable;})) === count($value)
|
||||
: ($value instanceof \Exception || $value instanceof \Throwable);
|
||||
}
|
||||
|
||||
public static function append(&$arr, $items)
|
||||
{
|
||||
if (is_array($items)) {
|
||||
$arr = array_merge($arr, $items);
|
||||
} else {
|
||||
$arr[] = $items;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for validators which determines if a value literal AST is valid given
|
||||
* an input type.
|
||||
*
|
||||
* Note that this only validates literal values, variables are assumed to
|
||||
* provide values of the correct type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function isValidLiteralValue(Type $type, $valueNode)
|
||||
{
|
||||
// A value must be provided if the type is non-null.
|
||||
if ($type instanceof NonNull) {
|
||||
if (!$valueNode || $valueNode instanceof NullValueNode) {
|
||||
return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ];
|
||||
}
|
||||
return static::isValidLiteralValue($type->getWrappedType(), $valueNode);
|
||||
}
|
||||
|
||||
if (!$valueNode || $valueNode instanceof NullValueNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// This function only tests literals, and assumes variables will provide
|
||||
// values of the correct type.
|
||||
if ($valueNode instanceof VariableNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lists accept a non-list value as a list of one.
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if ($valueNode instanceof ListValueNode) {
|
||||
$errors = [];
|
||||
foreach($valueNode->values as $index => $itemNode) {
|
||||
$tmp = static::isValidLiteralValue($itemType, $itemNode);
|
||||
|
||||
if ($tmp) {
|
||||
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
|
||||
return "In element #$index: $error";
|
||||
}));
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
} else {
|
||||
return static::isValidLiteralValue($itemType, $valueNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Input objects check each defined field and look for undefined fields.
|
||||
if ($type instanceof InputObjectType) {
|
||||
if ($valueNode->kind !== NodeKind::OBJECT) {
|
||||
return [ "Expected \"{$type->name}\", found not an object." ];
|
||||
}
|
||||
|
||||
$fields = $type->getFields();
|
||||
$errors = [];
|
||||
|
||||
// Ensure every provided field is defined.
|
||||
$fieldNodes = $valueNode->fields;
|
||||
|
||||
foreach ($fieldNodes as $providedFieldNode) {
|
||||
if (empty($fields[$providedFieldNode->name->value])) {
|
||||
$errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field.";
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure every defined field is valid.
|
||||
$fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;});
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
$result = static::isValidLiteralValue(
|
||||
$field->getType(),
|
||||
isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null
|
||||
);
|
||||
if ($result) {
|
||||
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
|
||||
return "In field \"$fieldName\": $error";
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if ($type instanceof LeafType) {
|
||||
// Scalars must parse to a non-null value
|
||||
if (!$type->isValidLiteral($valueNode)) {
|
||||
$printed = Printer::doPrint($valueNode);
|
||||
return [ "Expected type \"{$type->name}\", found $printed." ];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Must be input type');
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses a specialized visitor which runs multiple visitors in parallel,
|
||||
* while maintaining the visitor skip and break API.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param TypeInfo $typeInfo
|
||||
* @param DocumentNode $documentNode
|
||||
* @param AbstractValidationRule[] $rules
|
||||
* @return array
|
||||
*/
|
||||
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
|
||||
{
|
||||
$context = new ValidationContext($schema, $documentNode, $typeInfo);
|
||||
$visitors = [];
|
||||
foreach ($rules as $rule) {
|
||||
$visitors[] = $rule->getVisitor($context);
|
||||
}
|
||||
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
|
||||
return $context->getErrors();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
abstract class AbstractQuerySecurity extends AbstractValidationRule
|
||||
{
|
||||
const DISABLED = 0;
|
||||
|
||||
/**
|
||||
* @var FragmentDefinitionNode[]
|
||||
*/
|
||||
private $fragments = [];
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Language\AST\FragmentDefinitionNode[]
|
||||
*/
|
||||
protected function getFragments()
|
||||
{
|
||||
return $this->fragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*
|
||||
* @param $value
|
||||
*/
|
||||
protected function checkIfGreaterOrEqualToZero($name, $value)
|
||||
{
|
||||
if ($value < 0) {
|
||||
throw new \InvalidArgumentException(sprintf('$%s argument must be greater or equal to 0.', $name));
|
||||
}
|
||||
}
|
||||
|
||||
protected function gatherFragmentDefinition(ValidationContext $context)
|
||||
{
|
||||
// Gather all the fragment definition.
|
||||
// Importantly this does not include inline fragments.
|
||||
$definitions = $context->getDocument()->definitions;
|
||||
foreach ($definitions as $node) {
|
||||
if ($node instanceof FragmentDefinitionNode) {
|
||||
$this->fragments[$node->name->value] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFragment(FragmentSpreadNode $fragmentSpread)
|
||||
{
|
||||
$spreadName = $fragmentSpread->name->value;
|
||||
$fragments = $this->getFragments();
|
||||
|
||||
return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null;
|
||||
}
|
||||
|
||||
protected function invokeIfNeeded(ValidationContext $context, array $validators)
|
||||
{
|
||||
// is disabled?
|
||||
if (!$this->isEnabled()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->gatherFragmentDefinition($context);
|
||||
|
||||
return $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* Note: This is not the same as execution's collectFields because at static
|
||||
* time we do not know what object type will be used, so we unconditionally
|
||||
* spread in all fragments.
|
||||
*
|
||||
* @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
|
||||
*
|
||||
* @param ValidationContext $context
|
||||
* @param Type|null $parentType
|
||||
* @param SelectionSetNode $selectionSet
|
||||
* @param \ArrayObject $visitedFragmentNames
|
||||
* @param \ArrayObject $astAndDefs
|
||||
*
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
|
||||
{
|
||||
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
|
||||
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
|
||||
|
||||
foreach ($selectionSet->selections as $selection) {
|
||||
switch ($selection->kind) {
|
||||
case NodeKind::FIELD:
|
||||
/* @var FieldNode $selection */
|
||||
$fieldName = $selection->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType && method_exists($parentType, 'getFields')) {
|
||||
$tmp = $parentType->getFields();
|
||||
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
|
||||
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
|
||||
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
|
||||
|
||||
if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
|
||||
$fieldDef = $schemaMetaFieldDef;
|
||||
} elseif ($fieldName === $typeMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
|
||||
$fieldDef = $typeMetaFieldDef;
|
||||
} elseif ($fieldName === $typeNameMetaFieldDef->name) {
|
||||
$fieldDef = $typeNameMetaFieldDef;
|
||||
} elseif (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
}
|
||||
}
|
||||
$responseName = $this->getFieldName($selection);
|
||||
if (!isset($_astAndDefs[$responseName])) {
|
||||
$_astAndDefs[$responseName] = new \ArrayObject();
|
||||
}
|
||||
// create field context
|
||||
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
|
||||
break;
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
/* @var InlineFragmentNode $selection */
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
|
||||
$selection->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
break;
|
||||
case NodeKind::FRAGMENT_SPREAD:
|
||||
/* @var FragmentSpreadNode $selection */
|
||||
$fragName = $selection->name->value;
|
||||
|
||||
if (empty($_visitedFragmentNames[$fragName])) {
|
||||
$_visitedFragmentNames[$fragName] = true;
|
||||
$fragment = $context->getFragment($fragName);
|
||||
|
||||
if ($fragment) {
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition),
|
||||
$fragment->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $_astAndDefs;
|
||||
}
|
||||
|
||||
protected function getFieldName(FieldNode $node)
|
||||
{
|
||||
$fieldName = $node->name->value;
|
||||
$responseName = $node->alias ? $node->alias->value : $fieldName;
|
||||
|
||||
return $responseName;
|
||||
}
|
||||
|
||||
abstract protected function isEnabled();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
abstract class AbstractValidationRule
|
||||
{
|
||||
protected $name;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name ?: get_class($this);
|
||||
}
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return $this->getVisitor($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns structure suitable for GraphQL\Language\Visitor
|
||||
*
|
||||
* @see \GraphQL\Language\Visitor
|
||||
* @param ValidationContext $context
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getVisitor(ValidationContext $context);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class ArgumentsOfCorrectType extends AbstractValidationRule
|
||||
{
|
||||
static function badValueMessage($argName, $type, $value, $verboseErrors = [])
|
||||
{
|
||||
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
|
||||
return "Argument \"$argName\" has invalid value $value.$message";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::ARGUMENT => function(ArgumentNode $argNode) use ($context) {
|
||||
$argDef = $context->getArgument();
|
||||
if ($argDef) {
|
||||
$errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argNode->value);
|
||||
|
||||
if (!empty($errors)) {
|
||||
$context->reportError(new Error(
|
||||
self::badValueMessage($argNode->name->value, $argDef->getType(), Printer::doPrint($argNode->value), $errors),
|
||||
[$argNode->value]
|
||||
));
|
||||
}
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class CustomValidationRule extends AbstractValidationRule
|
||||
{
|
||||
private $visitorFn;
|
||||
|
||||
public function __construct($name, callable $visitorFn)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->visitorFn = $visitorFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ValidationContext $context
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$fn = $this->visitorFn;
|
||||
return $fn($context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class DefaultValuesOfCorrectType extends AbstractValidationRule
|
||||
{
|
||||
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
|
||||
{
|
||||
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
|
||||
return "Variable \$$varName has invalid default value: $value.$message";
|
||||
}
|
||||
|
||||
static function defaultForNonNullArgMessage($varName, $type, $guessType)
|
||||
{
|
||||
return "Variable \$$varName of type $type " .
|
||||
"is required and will never use the default value. " .
|
||||
"Perhaps you meant to use type $guessType.";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $varDefNode) use ($context) {
|
||||
$name = $varDefNode->variable->name->value;
|
||||
$defaultValue = $varDefNode->defaultValue;
|
||||
$type = $context->getInputType();
|
||||
|
||||
if ($type instanceof NonNull && $defaultValue) {
|
||||
$context->reportError(new Error(
|
||||
static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
|
||||
[$defaultValue]
|
||||
));
|
||||
}
|
||||
if ($type && $defaultValue) {
|
||||
$errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
|
||||
if (!empty($errors)) {
|
||||
$context->reportError(new Error(
|
||||
static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
|
||||
[$defaultValue]
|
||||
));
|
||||
}
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::SELECTION_SET => function() {return Visitor::skipNode();},
|
||||
NodeKind::FRAGMENT_DEFINITION => function() {return Visitor::skipNode();}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class DisableIntrospection extends AbstractQuerySecurity
|
||||
{
|
||||
const ENABLED = 1;
|
||||
private $isEnabled;
|
||||
|
||||
public function __construct($enabled = self::ENABLED)
|
||||
{
|
||||
$this->setEnabled($enabled);
|
||||
}
|
||||
|
||||
public function setEnabled($enabled)
|
||||
{
|
||||
$this->isEnabled = $enabled;
|
||||
}
|
||||
|
||||
static function introspectionDisabledMessage()
|
||||
{
|
||||
return 'GraphQL introspection is not allowed, but the query contained __schema or __type';
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->isEnabled !== static::DISABLED;
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return $this->invokeIfNeeded(
|
||||
$context,
|
||||
[
|
||||
NodeKind::FIELD => function (FieldNode $node) use ($context) {
|
||||
if ($node->name->value === '__type' || $node->name->value === '__schema') {
|
||||
$context->reportError(new Error(
|
||||
static::introspectionDisabledMessage(),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class FieldsOnCorrectType extends AbstractValidationRule
|
||||
{
|
||||
static function undefinedFieldMessage($field, $type, array $suggestedTypes = [])
|
||||
{
|
||||
$message = 'Cannot query field "' . $field . '" on type "' . $type.'".';
|
||||
|
||||
$maxLength = 5;
|
||||
$count = count($suggestedTypes);
|
||||
if ($count > 0) {
|
||||
$suggestions = array_slice($suggestedTypes, 0, $maxLength);
|
||||
$suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; });
|
||||
$suggestions = implode(', ', $suggestions);
|
||||
|
||||
if ($count > $maxLength) {
|
||||
$suggestions .= ', and ' . ($count - $maxLength) . ' other types';
|
||||
}
|
||||
$message .= " However, this field exists on $suggestions.";
|
||||
$message .= ' Perhaps you meant to use an inline fragment?';
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => function(FieldNode $node) use ($context) {
|
||||
$type = $context->getParentType();
|
||||
if ($type) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
if (!$fieldDef) {
|
||||
$context->reportError(new Error(
|
||||
static::undefinedFieldMessage($node->name->value, $type->name),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class FragmentsOnCompositeTypes extends AbstractValidationRule
|
||||
{
|
||||
static function inlineFragmentOnNonCompositeErrorMessage($type)
|
||||
{
|
||||
return "Fragment cannot condition on non composite type \"$type\".";
|
||||
}
|
||||
|
||||
static function fragmentOnNonCompositeErrorMessage($fragName, $type)
|
||||
{
|
||||
return "Fragment \"$fragName\" cannot condition on non composite type \"$type\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) {
|
||||
if ($node->typeCondition) {
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
|
||||
if ($type && !Type::isCompositeType($type)) {
|
||||
$context->reportError(new Error(
|
||||
static::inlineFragmentOnNonCompositeErrorMessage($type),
|
||||
[$node->typeCondition]
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
|
||||
|
||||
if ($type && !Type::isCompositeType($type)) {
|
||||
$context->reportError(new Error(
|
||||
static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
|
||||
[$node->typeCondition]
|
||||
));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownArgumentNames extends AbstractValidationRule
|
||||
{
|
||||
public static function unknownArgMessage($argName, $fieldName, $type)
|
||||
{
|
||||
return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\".";
|
||||
}
|
||||
|
||||
public static function unknownDirectiveArgMessage($argName, $directiveName)
|
||||
{
|
||||
return "Unknown argument \"$argName\" on directive \"@$directiveName\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
|
||||
$argumentOf = $ancestors[count($ancestors) - 1];
|
||||
if ($argumentOf->kind === NodeKind::FIELD) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
|
||||
if ($fieldDef) {
|
||||
$fieldArgDef = null;
|
||||
foreach ($fieldDef->args as $arg) {
|
||||
if ($arg->name === $node->name->value) {
|
||||
$fieldArgDef = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$fieldArgDef) {
|
||||
$parentType = $context->getParentType();
|
||||
Utils::invariant($parentType);
|
||||
$context->reportError(new Error(
|
||||
self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
|
||||
$directive = $context->getDirective();
|
||||
if ($directive) {
|
||||
$directiveArgDef = null;
|
||||
foreach ($directive->args as $arg) {
|
||||
if ($arg->name === $node->name->value) {
|
||||
$directiveArgDef = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$directiveArgDef) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownDirectiveArgMessage($node->name->value, $directive->name),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use GraphQL\Type\Definition\DirectiveLocation;
|
||||
|
||||
class KnownDirectives extends AbstractValidationRule
|
||||
{
|
||||
static function unknownDirectiveMessage($directiveName)
|
||||
{
|
||||
return "Unknown directive \"$directiveName\".";
|
||||
}
|
||||
|
||||
static function misplacedDirectiveMessage($directiveName, $location)
|
||||
{
|
||||
return "Directive \"$directiveName\" may not be used on \"$location\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::DIRECTIVE => function (DirectiveNode $node, $key, $parent, $path, $ancestors) use ($context) {
|
||||
$directiveDef = null;
|
||||
foreach ($context->getSchema()->getDirectives() as $def) {
|
||||
if ($def->name === $node->name->value) {
|
||||
$directiveDef = $def;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$directiveDef) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownDirectiveMessage($node->name->value),
|
||||
[$node]
|
||||
));
|
||||
return ;
|
||||
}
|
||||
$appliedTo = $ancestors[count($ancestors) - 1];
|
||||
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
|
||||
|
||||
if (!$candidateLocation) {
|
||||
$context->reportError(new Error(
|
||||
self::misplacedDirectiveMessage($node->name->value, $node->type),
|
||||
[$node]
|
||||
));
|
||||
} else if (!in_array($candidateLocation, $directiveDef->locations)) {
|
||||
$context->reportError(new Error(
|
||||
self::misplacedDirectiveMessage($node->name->value, $candidateLocation),
|
||||
[ $node ]
|
||||
));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function getLocationForAppliedNode(Node $appliedTo)
|
||||
{
|
||||
switch ($appliedTo->kind) {
|
||||
case NodeKind::OPERATION_DEFINITION:
|
||||
switch ($appliedTo->operation) {
|
||||
case 'query': return DirectiveLocation::QUERY;
|
||||
case 'mutation': return DirectiveLocation::MUTATION;
|
||||
case 'subscription': return DirectiveLocation::SUBSCRIPTION;
|
||||
}
|
||||
break;
|
||||
case NodeKind::FIELD: return DirectiveLocation::FIELD;
|
||||
case NodeKind::FRAGMENT_SPREAD: return DirectiveLocation::FRAGMENT_SPREAD;
|
||||
case NodeKind::INLINE_FRAGMENT: return DirectiveLocation::INLINE_FRAGMENT;
|
||||
case NodeKind::FRAGMENT_DEFINITION: return DirectiveLocation::FRAGMENT_DEFINITION;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownFragmentNames extends AbstractValidationRule
|
||||
{
|
||||
static function unknownFragmentMessage($fragName)
|
||||
{
|
||||
return "Unknown fragment \"$fragName\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
$fragment = $context->getFragment($fragmentName);
|
||||
if (!$fragment) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownFragmentMessage($fragmentName),
|
||||
[$node->name]
|
||||
));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownTypeNames extends AbstractValidationRule
|
||||
{
|
||||
static function unknownTypeMessage($type)
|
||||
{
|
||||
return "Unknown type \"$type\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$skip = function() {return Visitor::skipNode();};
|
||||
|
||||
return [
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
|
||||
NodeKind::UNION_TYPE_DEFINITION => $skip,
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
|
||||
|
||||
NodeKind::NAMED_TYPE => function(NamedTypeNode $node, $key) use ($context) {
|
||||
$typeName = $node->name->value;
|
||||
$type = $context->getSchema()->getType($typeName);
|
||||
if (!$type) {
|
||||
$context->reportError(new Error(self::unknownTypeMessage($typeName), [$node]));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
/**
|
||||
* Lone anonymous operation
|
||||
*
|
||||
* A GraphQL document is only valid if when it contains an anonymous operation
|
||||
* (the query short-hand) that it contains only that one operation definition.
|
||||
*/
|
||||
class LoneAnonymousOperation extends AbstractValidationRule
|
||||
{
|
||||
static function anonOperationNotAloneMessage()
|
||||
{
|
||||
return 'This anonymous operation must be the only defined operation.';
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$operationCount = 0;
|
||||
return [
|
||||
NodeKind::DOCUMENT => function(DocumentNode $node) use (&$operationCount) {
|
||||
$tmp = Utils::filter(
|
||||
$node->definitions,
|
||||
function ($definition) {
|
||||
return $definition->kind === NodeKind::OPERATION_DEFINITION;
|
||||
}
|
||||
);
|
||||
$operationCount = count($tmp);
|
||||
},
|
||||
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) {
|
||||
if (!$node->name && $operationCount > 1) {
|
||||
$context->reportError(
|
||||
new Error(self::anonOperationNotAloneMessage(), [$node])
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoFragmentCycles extends AbstractValidationRule
|
||||
{
|
||||
static function cycleErrorMessage($fragName, array $spreadNames = [])
|
||||
{
|
||||
$via = !empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '';
|
||||
return "Cannot spread fragment \"$fragName\" within itself$via.";
|
||||
}
|
||||
|
||||
public $visitedFrags;
|
||||
|
||||
public $spreadPath;
|
||||
|
||||
public $spreadPathIndexByName;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
// Tracks already visited fragments to maintain O(N) and to ensure that cycles
|
||||
// are not redundantly reported.
|
||||
$this->visitedFrags = [];
|
||||
|
||||
// Array of AST nodes used to produce meaningful errors
|
||||
$this->spreadPath = [];
|
||||
|
||||
// Position in the spread path
|
||||
$this->spreadPathIndexByName = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => function () {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
|
||||
if (!isset($this->visitedFrags[$node->name->value])) {
|
||||
$this->detectCycleRecursive($node, $context);
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context)
|
||||
{
|
||||
$fragmentName = $fragment->name->value;
|
||||
$this->visitedFrags[$fragmentName] = true;
|
||||
|
||||
$spreadNodes = $context->getFragmentSpreads($fragment);
|
||||
|
||||
if (empty($spreadNodes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
|
||||
|
||||
for ($i = 0; $i < count($spreadNodes); $i++) {
|
||||
$spreadNode = $spreadNodes[$i];
|
||||
$spreadName = $spreadNode->name->value;
|
||||
$cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null;
|
||||
|
||||
if ($cycleIndex === null) {
|
||||
$this->spreadPath[] = $spreadNode;
|
||||
if (empty($this->visitedFrags[$spreadName])) {
|
||||
$spreadFragment = $context->getFragment($spreadName);
|
||||
if ($spreadFragment) {
|
||||
$this->detectCycleRecursive($spreadFragment, $context);
|
||||
}
|
||||
}
|
||||
array_pop($this->spreadPath);
|
||||
} else {
|
||||
$cyclePath = array_slice($this->spreadPath, $cycleIndex);
|
||||
$nodes = $cyclePath;
|
||||
|
||||
if (is_array($spreadNode)) {
|
||||
$nodes = array_merge($nodes, $spreadNode);
|
||||
} else {
|
||||
$nodes[] = $spreadNode;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::cycleErrorMessage(
|
||||
$spreadName,
|
||||
Utils::map($cyclePath, function ($s) {
|
||||
return $s->name->value;
|
||||
})
|
||||
),
|
||||
$nodes
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->spreadPathIndexByName[$fragmentName] = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
/**
|
||||
* Class NoUndefinedVariables
|
||||
*
|
||||
* A GraphQL operation is only valid if all variables encountered, both directly
|
||||
* and via fragment spreads, are defined by that operation.
|
||||
*
|
||||
* @package GraphQL\Validator\Rules
|
||||
*/
|
||||
class NoUndefinedVariables extends AbstractValidationRule
|
||||
{
|
||||
static function undefinedVarMessage($varName, $opName = null)
|
||||
{
|
||||
return $opName
|
||||
? "Variable \"$$varName\" is not defined by operation \"$opName\"."
|
||||
: "Variable \"$$varName\" is not defined.";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$variableNameDefined = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'enter' => function() use (&$variableNameDefined) {
|
||||
$variableNameDefined = [];
|
||||
},
|
||||
'leave' => function(OperationDefinitionNode $operation) use (&$variableNameDefined, $context) {
|
||||
$usages = $context->getRecursiveVariableUsages($operation);
|
||||
|
||||
foreach ($usages as $usage) {
|
||||
$node = $usage['node'];
|
||||
$varName = $node->name->value;
|
||||
|
||||
if (empty($variableNameDefined[$varName])) {
|
||||
$context->reportError(new Error(
|
||||
self::undefinedVarMessage(
|
||||
$varName,
|
||||
$operation->name ? $operation->name->value : null
|
||||
),
|
||||
[ $node, $operation ]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $def) use (&$variableNameDefined) {
|
||||
$variableNameDefined[$def->variable->name->value] = true;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoUnusedFragments extends AbstractValidationRule
|
||||
{
|
||||
static function unusedFragMessage($fragName)
|
||||
{
|
||||
return "Fragment \"$fragName\" is never used.";
|
||||
}
|
||||
|
||||
public $operationDefs;
|
||||
|
||||
public $fragmentDefs;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->operationDefs = [];
|
||||
$this->fragmentDefs = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => function($node) {
|
||||
$this->operationDefs[] = $node;
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $def) {
|
||||
$this->fragmentDefs[] = $def;
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::DOCUMENT => [
|
||||
'leave' => function() use ($context) {
|
||||
$fragmentNameUsed = [];
|
||||
|
||||
foreach ($this->operationDefs as $operation) {
|
||||
foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) {
|
||||
$fragmentNameUsed[$fragment->name->value] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->fragmentDefs as $fragmentDef) {
|
||||
$fragName = $fragmentDef->name->value;
|
||||
if (empty($fragmentNameUsed[$fragName])) {
|
||||
$context->reportError(new Error(
|
||||
self::unusedFragMessage($fragName),
|
||||
[ $fragmentDef ]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoUnusedVariables extends AbstractValidationRule
|
||||
{
|
||||
static function unusedVariableMessage($varName, $opName = null)
|
||||
{
|
||||
return $opName
|
||||
? "Variable \"$$varName\" is never used in operation \"$opName\"."
|
||||
: "Variable \"$$varName\" is never used.";
|
||||
}
|
||||
|
||||
public $variableDefs;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->variableDefs = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'enter' => function() {
|
||||
$this->variableDefs = [];
|
||||
},
|
||||
'leave' => function(OperationDefinitionNode $operation) use ($context) {
|
||||
$variableNameUsed = [];
|
||||
$usages = $context->getRecursiveVariableUsages($operation);
|
||||
$opName = $operation->name ? $operation->name->value : null;
|
||||
|
||||
foreach ($usages as $usage) {
|
||||
$node = $usage['node'];
|
||||
$variableNameUsed[$node->name->value] = true;
|
||||
}
|
||||
|
||||
foreach ($this->variableDefs as $variableDef) {
|
||||
$variableName = $variableDef->variable->name->value;
|
||||
|
||||
if (empty($variableNameUsed[$variableName])) {
|
||||
$context->reportError(new Error(
|
||||
self::unusedVariableMessage($variableName, $opName),
|
||||
[$variableDef]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => function($def) {
|
||||
$this->variableDefs[] = $def;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Utils\PairSet;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class OverlappingFieldsCanBeMerged extends AbstractValidationRule
|
||||
{
|
||||
static function fieldsConflictMessage($responseName, $reason)
|
||||
{
|
||||
$reasonMessage = self::reasonMessage($reason);
|
||||
return "Fields \"$responseName\" conflict because $reasonMessage.";
|
||||
}
|
||||
|
||||
static function reasonMessage($reason)
|
||||
{
|
||||
if (is_array($reason)) {
|
||||
$tmp = array_map(function ($tmp) {
|
||||
list($responseName, $subReason) = $tmp;
|
||||
$reasonMessage = self::reasonMessage($subReason);
|
||||
return "subfields \"$responseName\" conflict because $reasonMessage";
|
||||
}, $reason);
|
||||
return implode(' and ', $tmp);
|
||||
}
|
||||
return $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var PairSet
|
||||
*/
|
||||
public $comparedSet;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->comparedSet = new PairSet();
|
||||
|
||||
return [
|
||||
NodeKind::SELECTION_SET => [
|
||||
// Note: we validate on the reverse traversal so deeper conflicts will be
|
||||
// caught first, for clearer error messages.
|
||||
'leave' => function(SelectionSetNode $selectionSet) use ($context) {
|
||||
$fieldMap = $this->collectFieldNodesAndDefs(
|
||||
$context,
|
||||
$context->getParentType(),
|
||||
$selectionSet
|
||||
);
|
||||
|
||||
$conflicts = $this->findConflicts(false, $fieldMap, $context);
|
||||
|
||||
foreach ($conflicts as $conflict) {
|
||||
$responseName = $conflict[0][0];
|
||||
$reason = $conflict[0][1];
|
||||
$fields1 = $conflict[1];
|
||||
$fields2 = $conflict[2];
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::fieldsConflictMessage($responseName, $reason),
|
||||
array_merge($fields1, $fields2)
|
||||
));
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function findConflicts($parentFieldsAreMutuallyExclusive, $fieldMap, ValidationContext $context)
|
||||
{
|
||||
$conflicts = [];
|
||||
foreach ($fieldMap as $responseName => $fields) {
|
||||
$count = count($fields);
|
||||
if ($count > 1) {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
for ($j = $i; $j < $count; $j++) {
|
||||
$conflict = $this->findConflict(
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
$fields[$i],
|
||||
$fields[$j],
|
||||
$context
|
||||
);
|
||||
|
||||
if ($conflict) {
|
||||
$conflicts[] = $conflict;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $parentFieldsAreMutuallyExclusive
|
||||
* @param $responseName
|
||||
* @param [FieldNode, GraphQLFieldDefinition] $pair1
|
||||
* @param [FieldNode, GraphQLFieldDefinition] $pair2
|
||||
* @param ValidationContext $context
|
||||
* @return array|null
|
||||
*/
|
||||
private function findConflict(
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
array $pair1,
|
||||
array $pair2,
|
||||
ValidationContext $context
|
||||
)
|
||||
{
|
||||
list($parentType1, $ast1, $def1) = $pair1;
|
||||
list($parentType2, $ast2, $def2) = $pair2;
|
||||
|
||||
// Not a pair.
|
||||
if ($ast1 === $ast2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Memoize, do not report the same issue twice.
|
||||
// Note: Two overlapping ASTs could be encountered both when
|
||||
// `parentFieldsAreMutuallyExclusive` is true and is false, which could
|
||||
// produce different results (when `true` being a subset of `false`).
|
||||
// However we do not need to include this piece of information when
|
||||
// memoizing since this rule visits leaf fields before their parent fields,
|
||||
// ensuring that `parentFieldsAreMutuallyExclusive` is `false` the first
|
||||
// time two overlapping fields are encountered, ensuring that the full
|
||||
// set of validation rules are always checked when necessary.
|
||||
if ($this->comparedSet->has($ast1, $ast2)) {
|
||||
return null;
|
||||
}
|
||||
$this->comparedSet->add($ast1, $ast2);
|
||||
|
||||
// The return type for each field.
|
||||
$type1 = isset($def1) ? $def1->getType() : null;
|
||||
$type2 = isset($def2) ? $def2->getType() : null;
|
||||
|
||||
// If it is known that two fields could not possibly apply at the same
|
||||
// time, due to the parent types, then it is safe to permit them to diverge
|
||||
// in aliased field or arguments used as they will not present any ambiguity
|
||||
// by differing.
|
||||
// It is known that two parent types could never overlap if they are
|
||||
// different Object types. Interface or Union types might overlap - if not
|
||||
// in the current state of the schema, then perhaps in some future version,
|
||||
// thus may not safely diverge.
|
||||
$fieldsAreMutuallyExclusive =
|
||||
$parentFieldsAreMutuallyExclusive ||
|
||||
$parentType1 !== $parentType2 &&
|
||||
$parentType1 instanceof ObjectType &&
|
||||
$parentType2 instanceof ObjectType;
|
||||
|
||||
if (!$fieldsAreMutuallyExclusive) {
|
||||
$name1 = $ast1->name->value;
|
||||
$name2 = $ast2->name->value;
|
||||
|
||||
if ($name1 !== $name2) {
|
||||
return [
|
||||
[$responseName, "$name1 and $name2 are different fields"],
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$args1 = isset($ast1->arguments) ? $ast1->arguments : [];
|
||||
$args2 = isset($ast2->arguments) ? $ast2->arguments : [];
|
||||
|
||||
if (!$this->sameArguments($args1, $args2)) {
|
||||
return [
|
||||
[$responseName, 'they have differing arguments'],
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) {
|
||||
return [
|
||||
[$responseName, "they return conflicting types $type1 and $type2"],
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$subfieldMap = $this->getSubfieldMap($ast1, $type1, $ast2, $type2, $context);
|
||||
|
||||
if ($subfieldMap) {
|
||||
$conflicts = $this->findConflicts($fieldsAreMutuallyExclusive, $subfieldMap, $context);
|
||||
return $this->subfieldConflicts($conflicts, $responseName, $ast1, $ast2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getSubfieldMap(
|
||||
FieldNode $ast1,
|
||||
$type1,
|
||||
FieldNode $ast2,
|
||||
$type2,
|
||||
ValidationContext $context
|
||||
) {
|
||||
$selectionSet1 = $ast1->selectionSet;
|
||||
$selectionSet2 = $ast2->selectionSet;
|
||||
if ($selectionSet1 && $selectionSet2) {
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
$subfieldMap = $this->collectFieldNodesAndDefs(
|
||||
$context,
|
||||
Type::getNamedType($type1),
|
||||
$selectionSet1,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
$subfieldMap = $this->collectFieldNodesAndDefs(
|
||||
$context,
|
||||
Type::getNamedType($type2),
|
||||
$selectionSet2,
|
||||
$visitedFragmentNames,
|
||||
$subfieldMap
|
||||
);
|
||||
return $subfieldMap;
|
||||
}
|
||||
}
|
||||
|
||||
private function subfieldConflicts(
|
||||
array $conflicts,
|
||||
$responseName,
|
||||
FieldNode $ast1,
|
||||
FieldNode $ast2
|
||||
)
|
||||
{
|
||||
if (!empty($conflicts)) {
|
||||
return [
|
||||
[
|
||||
$responseName,
|
||||
Utils::map($conflicts, function($conflict) {return $conflict[0];})
|
||||
],
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
function($allFields, $conflict) { return array_merge($allFields, $conflict[1]);},
|
||||
[ $ast1 ]
|
||||
),
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
function($allFields, $conflict) {return array_merge($allFields, $conflict[2]);},
|
||||
[ $ast2 ]
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputType $type1
|
||||
* @param OutputType $type2
|
||||
* @return bool
|
||||
*/
|
||||
private function doTypesConflict(OutputType $type1, OutputType $type2)
|
||||
{
|
||||
if ($type1 instanceof ListOfType) {
|
||||
return $type2 instanceof ListOfType ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type2 instanceof ListOfType) {
|
||||
return $type1 instanceof ListOfType ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type1 instanceof NonNull) {
|
||||
return $type2 instanceof NonNull ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type2 instanceof NonNull) {
|
||||
return $type1 instanceof NonNull ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
|
||||
return $type1 !== $type2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* Note: This is not the same as execution's collectFields because at static
|
||||
* time we do not know what object type will be used, so we unconditionally
|
||||
* spread in all fragments.
|
||||
*
|
||||
* @param ValidationContext $context
|
||||
* @param mixed $parentType
|
||||
* @param SelectionSetNode $selectionSet
|
||||
* @param \ArrayObject $visitedFragmentNames
|
||||
* @param \ArrayObject $astAndDefs
|
||||
* @return mixed
|
||||
*/
|
||||
private function collectFieldNodesAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
|
||||
{
|
||||
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
|
||||
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
|
||||
|
||||
for ($i = 0; $i < count($selectionSet->selections); $i++) {
|
||||
$selection = $selectionSet->selections[$i];
|
||||
|
||||
switch ($selection->kind) {
|
||||
case NodeKind::FIELD:
|
||||
$fieldName = $selection->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType && method_exists($parentType, 'getFields')) {
|
||||
$tmp = $parentType->getFields();
|
||||
if (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
}
|
||||
}
|
||||
$responseName = $selection->alias ? $selection->alias->value : $fieldName;
|
||||
|
||||
if (!isset($_astAndDefs[$responseName])) {
|
||||
$_astAndDefs[$responseName] = new \ArrayObject();
|
||||
}
|
||||
$_astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
|
||||
break;
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
$typeCondition = $selection->typeCondition;
|
||||
$inlineFragmentType = $typeCondition
|
||||
? TypeInfo::typeFromAST($context->getSchema(), $typeCondition)
|
||||
: $parentType;
|
||||
|
||||
$_astAndDefs = $this->collectFieldNodesAndDefs(
|
||||
$context,
|
||||
$inlineFragmentType,
|
||||
$selection->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
break;
|
||||
case NodeKind::FRAGMENT_SPREAD:
|
||||
/** @var FragmentSpreadNode $selection */
|
||||
$fragName = $selection->name->value;
|
||||
if (!empty($_visitedFragmentNames[$fragName])) {
|
||||
continue;
|
||||
}
|
||||
$_visitedFragmentNames[$fragName] = true;
|
||||
$fragment = $context->getFragment($fragName);
|
||||
if (!$fragment) {
|
||||
continue;
|
||||
}
|
||||
$fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition);
|
||||
$_astAndDefs = $this->collectFieldNodesAndDefs(
|
||||
$context,
|
||||
$fragmentType,
|
||||
$fragment->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $_astAndDefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArgumentNode[]|DirectiveNode[] $arguments1
|
||||
* @param ArgumentNode[]|DirectiveNode[] $arguments2
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
private function sameArguments($arguments1, $arguments2)
|
||||
{
|
||||
if (count($arguments1) !== count($arguments2)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($arguments1 as $arg1) {
|
||||
$arg2 = null;
|
||||
foreach ($arguments2 as $arg) {
|
||||
if ($arg->name->value === $arg1->name->value) {
|
||||
$arg2 = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$arg2) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->sameValue($arg1->value, $arg2->value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function sameValue($value1, $value2)
|
||||
{
|
||||
return (!$value1 && !$value2) || (Printer::doPrint($value1) === Printer::doPrint($value2));
|
||||
}
|
||||
|
||||
function sameType($type1, $type2)
|
||||
{
|
||||
return (string) $type1 === (string) $type2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
class PossibleFragmentSpreads extends AbstractValidationRule
|
||||
{
|
||||
static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
|
||||
{
|
||||
return "Fragment \"$fragName\" cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
|
||||
}
|
||||
|
||||
static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
|
||||
{
|
||||
return "Fragment cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) {
|
||||
$fragType = $context->getType();
|
||||
$parentType = $context->getParentType();
|
||||
|
||||
if ($fragType instanceof CompositeType &&
|
||||
$parentType instanceof CompositeType &&
|
||||
!$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
|
||||
$context->reportError(new Error(
|
||||
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
},
|
||||
NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) {
|
||||
$fragName = $node->name->value;
|
||||
$fragType = $this->getFragmentType($context, $fragName);
|
||||
$parentType = $context->getParentType();
|
||||
|
||||
if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
|
||||
$context->reportError(new Error(
|
||||
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function getFragmentType(ValidationContext $context, $name)
|
||||
{
|
||||
$frag = $context->getFragment($name);
|
||||
return $frag ? TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
|
||||
}
|
||||
|
||||
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
|
||||
{
|
||||
// Checking in the order of the most frequently used scenarios:
|
||||
// Parent type === fragment type
|
||||
if ($parentType === $fragType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parent type is interface or union, fragment type is object type
|
||||
if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
|
||||
return $schema->isPossibleType($parentType, $fragType);
|
||||
}
|
||||
|
||||
// Parent type is object type, fragment type is interface (or rather rare - union)
|
||||
if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
|
||||
return $schema->isPossibleType($fragType, $parentType);
|
||||
}
|
||||
|
||||
// Both are object types:
|
||||
if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) {
|
||||
return $parentType === $fragType;
|
||||
}
|
||||
|
||||
// Both are interfaces
|
||||
// This case may be assumed valid only when implementations of two interfaces intersect
|
||||
// But we don't have information about all implementations at runtime
|
||||
// (getting this information via $schema->getPossibleTypes() requires scanning through whole schema
|
||||
// which is very costly to do at each request due to PHP "shared nothing" architecture)
|
||||
//
|
||||
// So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution
|
||||
// See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602
|
||||
if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) {
|
||||
return true;
|
||||
|
||||
// Note that there is one case when we do have information about all implementations:
|
||||
// When schema descriptor is defined ($schema->hasDescriptor())
|
||||
// BUT we must avoid situation when some query that worked in development had suddenly stopped
|
||||
// working in production. So staying consistent and always validate.
|
||||
}
|
||||
|
||||
// Interface within union
|
||||
if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) {
|
||||
foreach ($parentType->getTypes() as $type) {
|
||||
if ($type->implementsInterface($fragType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) {
|
||||
foreach ($fragType->getTypes() as $type) {
|
||||
if ($type->implementsInterface($parentType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentType instanceof UnionType && $fragType instanceof UnionType) {
|
||||
foreach ($fragType->getTypes() as $type) {
|
||||
if ($parentType->isPossibleType($type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class ProvidedNonNullArguments extends AbstractValidationRule
|
||||
{
|
||||
static function missingFieldArgMessage($fieldName, $argName, $type)
|
||||
{
|
||||
return "Field \"$fieldName\" argument \"$argName\" of type \"$type\" is required but not provided.";
|
||||
}
|
||||
|
||||
static function missingDirectiveArgMessage($directiveName, $argName, $type)
|
||||
{
|
||||
return "Directive \"@$directiveName\" argument \"$argName\" of type \"$type\" is required but not provided.";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => [
|
||||
'leave' => function(FieldNode $fieldNode) use ($context) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
|
||||
if (!$fieldDef) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$argNodes = $fieldNode->arguments ?: [];
|
||||
|
||||
$argNodeMap = [];
|
||||
foreach ($argNodes as $argNode) {
|
||||
$argNodeMap[$argNode->name->value] = $argNodes;
|
||||
}
|
||||
foreach ($fieldDef->args as $argDef) {
|
||||
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
|
||||
if (!$argNode && $argDef->getType() instanceof NonNull) {
|
||||
$context->reportError(new Error(
|
||||
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
|
||||
[$fieldNode]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
NodeKind::DIRECTIVE => [
|
||||
'leave' => function(DirectiveNode $directiveNode) use ($context) {
|
||||
$directiveDef = $context->getDirective();
|
||||
if (!$directiveDef) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$argNodes = $directiveNode->arguments ?: [];
|
||||
$argNodeMap = [];
|
||||
foreach ($argNodes as $argNode) {
|
||||
$argNodeMap[$argNode->name->value] = $argNodes;
|
||||
}
|
||||
|
||||
foreach ($directiveDef->args as $argDef) {
|
||||
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
|
||||
if (!$argNode && $argDef->getType() instanceof NonNull) {
|
||||
$context->reportError(new Error(
|
||||
self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()),
|
||||
[$directiveNode]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class QueryComplexity extends AbstractQuerySecurity
|
||||
{
|
||||
private $maxQueryComplexity;
|
||||
|
||||
private $rawVariableValues = [];
|
||||
|
||||
private $variableDefs;
|
||||
|
||||
private $fieldNodeAndDefs;
|
||||
|
||||
/**
|
||||
* @var ValidationContext
|
||||
*/
|
||||
private $context;
|
||||
|
||||
public function __construct($maxQueryComplexity)
|
||||
{
|
||||
$this->setMaxQueryComplexity($maxQueryComplexity);
|
||||
}
|
||||
|
||||
public static function maxQueryComplexityErrorMessage($max, $count)
|
||||
{
|
||||
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*
|
||||
* @param $maxQueryComplexity
|
||||
*/
|
||||
public function setMaxQueryComplexity($maxQueryComplexity)
|
||||
{
|
||||
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
|
||||
|
||||
$this->maxQueryComplexity = (int) $maxQueryComplexity;
|
||||
}
|
||||
|
||||
public function getMaxQueryComplexity()
|
||||
{
|
||||
return $this->maxQueryComplexity;
|
||||
}
|
||||
|
||||
public function setRawVariableValues(array $rawVariableValues = null)
|
||||
{
|
||||
$this->rawVariableValues = $rawVariableValues ?: [];
|
||||
}
|
||||
|
||||
public function getRawVariableValues()
|
||||
{
|
||||
return $this->rawVariableValues;
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
$this->variableDefs = new \ArrayObject();
|
||||
$this->fieldNodeAndDefs = new \ArrayObject();
|
||||
$complexity = 0;
|
||||
|
||||
return $this->invokeIfNeeded(
|
||||
$context,
|
||||
[
|
||||
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) {
|
||||
$this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
$context->getParentType(),
|
||||
$selectionSet,
|
||||
null,
|
||||
$this->fieldNodeAndDefs
|
||||
);
|
||||
},
|
||||
NodeKind::VARIABLE_DEFINITION => function ($def) {
|
||||
$this->variableDefs[] = $def;
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
|
||||
$errors = $context->getErrors();
|
||||
|
||||
if (empty($errors)) {
|
||||
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
|
||||
|
||||
if ($complexity > $this->getMaxQueryComplexity()) {
|
||||
$context->reportError(
|
||||
new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity))
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function fieldComplexity($node, $complexity = 0)
|
||||
{
|
||||
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
|
||||
foreach ($node->selectionSet->selections as $childNode) {
|
||||
$complexity = $this->nodeComplexity($childNode, $complexity);
|
||||
}
|
||||
}
|
||||
|
||||
return $complexity;
|
||||
}
|
||||
|
||||
private function nodeComplexity(Node $node, $complexity = 0)
|
||||
{
|
||||
switch ($node->kind) {
|
||||
case NodeKind::FIELD:
|
||||
/* @var FieldNode $node */
|
||||
// default values
|
||||
$args = [];
|
||||
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
|
||||
|
||||
// calculate children complexity if needed
|
||||
$childrenComplexity = 0;
|
||||
|
||||
// node has children?
|
||||
if (isset($node->selectionSet)) {
|
||||
$childrenComplexity = $this->fieldComplexity($node);
|
||||
}
|
||||
|
||||
$astFieldInfo = $this->astFieldInfo($node);
|
||||
$fieldDef = $astFieldInfo[1];
|
||||
|
||||
if ($fieldDef instanceof FieldDefinition) {
|
||||
if ($this->directiveExcludesField($node)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$args = $this->buildFieldArguments($node);
|
||||
//get complexity fn using fieldDef complexity
|
||||
if (method_exists($fieldDef, 'getComplexityFn')) {
|
||||
$complexityFn = $fieldDef->getComplexityFn();
|
||||
}
|
||||
}
|
||||
|
||||
$complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
|
||||
break;
|
||||
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
/* @var InlineFragmentNode $node */
|
||||
// node has children?
|
||||
if (isset($node->selectionSet)) {
|
||||
$complexity = $this->fieldComplexity($node, $complexity);
|
||||
}
|
||||
break;
|
||||
|
||||
case NodeKind::FRAGMENT_SPREAD:
|
||||
/* @var FragmentSpreadNode $node */
|
||||
$fragment = $this->getFragment($node);
|
||||
|
||||
if (null !== $fragment) {
|
||||
$complexity = $this->fieldComplexity($fragment, $complexity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $complexity;
|
||||
}
|
||||
|
||||
private function astFieldInfo(FieldNode $field)
|
||||
{
|
||||
$fieldName = $this->getFieldName($field);
|
||||
$astFieldInfo = [null, null];
|
||||
if (isset($this->fieldNodeAndDefs[$fieldName])) {
|
||||
foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
|
||||
if ($astAndDef[0] == $field) {
|
||||
$astFieldInfo = $astAndDef;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $astFieldInfo;
|
||||
}
|
||||
|
||||
private function buildFieldArguments(FieldNode $node)
|
||||
{
|
||||
$rawVariableValues = $this->getRawVariableValues();
|
||||
$astFieldInfo = $this->astFieldInfo($node);
|
||||
$fieldDef = $astFieldInfo[1];
|
||||
|
||||
$args = [];
|
||||
|
||||
if ($fieldDef instanceof FieldDefinition) {
|
||||
$variableValues = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$rawVariableValues
|
||||
);
|
||||
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private function directiveExcludesField(FieldNode $node) {
|
||||
foreach ($node->directives as $directiveNode) {
|
||||
if ($directiveNode->name->value === 'deprecated') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$variableValues = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$this->getRawVariableValues()
|
||||
);
|
||||
|
||||
if ($directiveNode->name->value === 'include') {
|
||||
$directive = Directive::includeDirective();
|
||||
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
||||
|
||||
return !$directiveArgs['if'];
|
||||
} else {
|
||||
$directive = Directive::skipDirective();
|
||||
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
||||
|
||||
return $directiveArgs['if'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->getMaxQueryComplexity() !== static::DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class QueryDepth extends AbstractQuerySecurity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxQueryDepth;
|
||||
|
||||
public function __construct($maxQueryDepth)
|
||||
{
|
||||
$this->setMaxQueryDepth($maxQueryDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*
|
||||
* @param $maxQueryDepth
|
||||
*/
|
||||
public function setMaxQueryDepth($maxQueryDepth)
|
||||
{
|
||||
$this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
|
||||
|
||||
$this->maxQueryDepth = (int) $maxQueryDepth;
|
||||
}
|
||||
|
||||
public function getMaxQueryDepth()
|
||||
{
|
||||
return $this->maxQueryDepth;
|
||||
}
|
||||
|
||||
public static function maxQueryDepthErrorMessage($max, $count)
|
||||
{
|
||||
return sprintf('Max query depth should be %d but got %d.', $max, $count);
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return $this->invokeIfNeeded(
|
||||
$context,
|
||||
[
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) {
|
||||
$maxDepth = $this->fieldDepth($operationDefinition);
|
||||
|
||||
if ($maxDepth > $this->getMaxQueryDepth()) {
|
||||
$context->reportError(
|
||||
new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->getMaxQueryDepth() !== static::DISABLED;
|
||||
}
|
||||
|
||||
private function fieldDepth($node, $depth = 0, $maxDepth = 0)
|
||||
{
|
||||
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
|
||||
foreach ($node->selectionSet->selections as $childNode) {
|
||||
$maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return $maxDepth;
|
||||
}
|
||||
|
||||
private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0)
|
||||
{
|
||||
switch ($node->kind) {
|
||||
case NodeKind::FIELD:
|
||||
/* @var FieldNode $node */
|
||||
// node has children?
|
||||
if (null !== $node->selectionSet) {
|
||||
// update maxDepth if needed
|
||||
if ($depth > $maxDepth) {
|
||||
$maxDepth = $depth;
|
||||
}
|
||||
$maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth);
|
||||
}
|
||||
break;
|
||||
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
/* @var InlineFragmentNode $node */
|
||||
// node has children?
|
||||
if (null !== $node->selectionSet) {
|
||||
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
|
||||
}
|
||||
break;
|
||||
|
||||
case NodeKind::FRAGMENT_SPREAD:
|
||||
/* @var FragmentSpreadNode $node */
|
||||
$fragment = $this->getFragment($node);
|
||||
|
||||
if (null !== $fragment) {
|
||||
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $maxDepth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class ScalarLeafs extends AbstractValidationRule
|
||||
{
|
||||
static function noSubselectionAllowedMessage($field, $type)
|
||||
{
|
||||
return "Field \"$field\" of type \"$type\" must not have a sub selection.";
|
||||
}
|
||||
|
||||
static function requiredSubselectionMessage($field, $type)
|
||||
{
|
||||
return "Field \"$field\" of type \"$type\" must have a sub selection.";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => function(FieldNode $node) use ($context) {
|
||||
$type = $context->getType();
|
||||
if ($type) {
|
||||
if (Type::isLeafType(Type::getNamedType($type))) {
|
||||
if ($node->selectionSet) {
|
||||
$context->reportError(new Error(
|
||||
self::noSubselectionAllowedMessage($node->name->value, $type),
|
||||
[$node->selectionSet]
|
||||
));
|
||||
}
|
||||
} else if (!$node->selectionSet) {
|
||||
$context->reportError(new Error(
|
||||
self::requiredSubselectionMessage($node->name->value, $type),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueArgumentNames extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateArgMessage($argName)
|
||||
{
|
||||
return "There can be only one argument named \"$argName\".";
|
||||
}
|
||||
|
||||
public $knownArgNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownArgNames = [];
|
||||
|
||||
return [
|
||||
NodeKind::FIELD => function () {
|
||||
$this->knownArgNames = [];;
|
||||
},
|
||||
NodeKind::DIRECTIVE => function () {
|
||||
$this->knownArgNames = [];
|
||||
},
|
||||
NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) {
|
||||
$argName = $node->name->value;
|
||||
if (!empty($this->knownArgNames[$argName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateArgMessage($argName),
|
||||
[$this->knownArgNames[$argName], $node->name]
|
||||
));
|
||||
} else {
|
||||
$this->knownArgNames[$argName] = $node->name;
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueDirectivesPerLocation extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateDirectiveMessage($directiveName)
|
||||
{
|
||||
return 'The directive "'.$directiveName.'" can only be used once at this location.';
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
'enter' => function(Node $node) use ($context) {
|
||||
if (isset($node->directives)) {
|
||||
$knownDirectives = [];
|
||||
foreach ($node->directives as $directive) {
|
||||
/** @var DirectiveNode $directive */
|
||||
$directiveName = $directive->name->value;
|
||||
if (isset($knownDirectives[$directiveName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateDirectiveMessage($directiveName),
|
||||
[$knownDirectives[$directiveName], $directive]
|
||||
));
|
||||
} else {
|
||||
$knownDirectives[$directiveName] = $directive;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueFragmentNames extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateFragmentNameMessage($fragName)
|
||||
{
|
||||
return "There can be only one fragment named \"$fragName\".";
|
||||
}
|
||||
|
||||
public $knownFragmentNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownFragmentNames = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => function () {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
if (!empty($this->knownFragmentNames[$fragmentName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateFragmentNameMessage($fragmentName),
|
||||
[ $this->knownFragmentNames[$fragmentName], $node->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownFragmentNames[$fragmentName] = $node->name;
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueInputFieldNames extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateInputFieldMessage($fieldName)
|
||||
{
|
||||
return "There can be only one input field named \"$fieldName\".";
|
||||
}
|
||||
|
||||
public $knownNames;
|
||||
public $knownNameStack;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownNames = [];
|
||||
$this->knownNameStack = [];
|
||||
|
||||
return [
|
||||
NodeKind::OBJECT => [
|
||||
'enter' => function() {
|
||||
$this->knownNameStack[] = $this->knownNames;
|
||||
$this->knownNames = [];
|
||||
},
|
||||
'leave' => function() {
|
||||
$this->knownNames = array_pop($this->knownNameStack);
|
||||
}
|
||||
],
|
||||
NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) {
|
||||
$fieldName = $node->name->value;
|
||||
|
||||
if (!empty($this->knownNames[$fieldName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateInputFieldMessage($fieldName),
|
||||
[ $this->knownNames[$fieldName], $node->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownNames[$fieldName] = $node->name;
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueOperationNames extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateOperationNameMessage($operationName)
|
||||
{
|
||||
return "There can be only one operation named \"$operationName\".";
|
||||
}
|
||||
|
||||
public $knownOperationNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownOperationNames = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use ($context) {
|
||||
$operationName = $node->name;
|
||||
|
||||
if ($operationName) {
|
||||
if (!empty($this->knownOperationNames[$operationName->value])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateOperationNameMessage($operationName->value),
|
||||
[ $this->knownOperationNames[$operationName->value], $operationName ]
|
||||
));
|
||||
} else {
|
||||
$this->knownOperationNames[$operationName->value] = $operationName;
|
||||
}
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function() {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueVariableNames extends AbstractValidationRule
|
||||
{
|
||||
static function duplicateVariableMessage($variableName)
|
||||
{
|
||||
return "There can be only one variable named \"$variableName\".";
|
||||
}
|
||||
|
||||
public $knownVariableNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownVariableNames = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => function() {
|
||||
$this->knownVariableNames = [];
|
||||
},
|
||||
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
|
||||
$variableName = $node->variable->name->value;
|
||||
if (!empty($this->knownVariableNames[$variableName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateVariableMessage($variableName),
|
||||
[ $this->knownVariableNames[$variableName], $node->variable->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownVariableNames[$variableName] = $node->variable->name;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class VariablesAreInputTypes extends AbstractValidationRule
|
||||
{
|
||||
static function nonInputTypeOnVarMessage($variableName, $typeName)
|
||||
{
|
||||
return "Variable \"\$$variableName\" cannot be non-input type \"$typeName\".";
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $node->type);
|
||||
|
||||
// If the variable type is not an input type, return an error.
|
||||
if ($type && !Type::isInputType($type)) {
|
||||
$variableName = $node->variable->name->value;
|
||||
$context->reportError(new Error(
|
||||
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
|
||||
[ $node->type ]
|
||||
));
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Utils\TypeComparators;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class VariablesInAllowedPosition extends AbstractValidationRule
|
||||
{
|
||||
static function badVarPosMessage($varName, $varType, $expectedType)
|
||||
{
|
||||
return "Variable \"\$$varName\" of type \"$varType\" used in position expecting ".
|
||||
"type \"$expectedType\".";
|
||||
}
|
||||
|
||||
public $varDefMap;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'enter' => function () {
|
||||
$this->varDefMap = [];
|
||||
},
|
||||
'leave' => function(OperationDefinitionNode $operation) use ($context) {
|
||||
$usages = $context->getRecursiveVariableUsages($operation);
|
||||
|
||||
foreach ($usages as $usage) {
|
||||
$node = $usage['node'];
|
||||
$type = $usage['type'];
|
||||
$varName = $node->name->value;
|
||||
$varDef = isset($this->varDefMap[$varName]) ? $this->varDefMap[$varName] : null;
|
||||
|
||||
if ($varDef && $type) {
|
||||
// A var type is allowed if it is the same or more strict (e.g. is
|
||||
// a subtype of) than the expected type. It can be more strict if
|
||||
// the variable type is non-null when the expected type is nullable.
|
||||
// If both are list types, the variable item type can be more strict
|
||||
// than the expected item type (contravariant).
|
||||
$schema = $context->getSchema();
|
||||
$varType = TypeInfo::typeFromAST($schema, $varDef->type);
|
||||
|
||||
if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) {
|
||||
$context->reportError(new Error(
|
||||
self::badVarPosMessage($varName, $varType, $type),
|
||||
[$varDef, $node]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) {
|
||||
$this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// A var type is allowed if it is the same or more strict than the expected
|
||||
// type. It can be more strict if the variable type is non-null when the
|
||||
// expected type is nullable. If both are list types, the variable item type can
|
||||
// be more strict than the expected item type.
|
||||
private function varTypeAllowedForType($varType, $expectedType)
|
||||
{
|
||||
if ($expectedType instanceof NonNull) {
|
||||
if ($varType instanceof NonNull) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ($varType instanceof NonNull) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType);
|
||||
}
|
||||
if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
|
||||
}
|
||||
return $varType === $expectedType;
|
||||
}
|
||||
|
||||
// If a variable definition has a default value, it's effectively non-null.
|
||||
private function effectiveType($varType, $varDef)
|
||||
{
|
||||
return (!$varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\HasSelectionSet;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use \SplObjectStorage;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
/**
|
||||
* An instance of this class is passed as the "this" context to all validators,
|
||||
* allowing access to commonly useful contextual information from within a
|
||||
* validation rule.
|
||||
*/
|
||||
class ValidationContext
|
||||
{
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
/**
|
||||
* @var DocumentNode
|
||||
*/
|
||||
private $ast;
|
||||
|
||||
/**
|
||||
* @var TypeInfo
|
||||
*/
|
||||
private $typeInfo;
|
||||
|
||||
/**
|
||||
* @var Error[]
|
||||
*/
|
||||
private $errors;
|
||||
|
||||
/**
|
||||
* @var FragmentDefinitionNode[]
|
||||
*/
|
||||
private $fragments;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $fragmentSpreads;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $recursivelyReferencedFragments;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $variableUsages;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $recursiveVariableUsages;
|
||||
|
||||
/**
|
||||
* ValidationContext constructor.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param DocumentNode $ast
|
||||
* @param TypeInfo $typeInfo
|
||||
*/
|
||||
function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->ast = $ast;
|
||||
$this->typeInfo = $typeInfo;
|
||||
$this->errors = [];
|
||||
$this->fragmentSpreads = new SplObjectStorage();
|
||||
$this->recursivelyReferencedFragments = new SplObjectStorage();
|
||||
$this->variableUsages = new SplObjectStorage();
|
||||
$this->recursiveVariableUsages = new SplObjectStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Error $error
|
||||
*/
|
||||
function reportError(Error $error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]
|
||||
*/
|
||||
function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
function getSchema()
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DocumentNode
|
||||
*/
|
||||
function getDocument()
|
||||
{
|
||||
return $this->ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return FragmentDefinitionNode|null
|
||||
*/
|
||||
function getFragment($name)
|
||||
{
|
||||
$fragments = $this->fragments;
|
||||
if (!$fragments) {
|
||||
$fragments = [];
|
||||
foreach ($this->getDocument()->definitions as $statement) {
|
||||
if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) {
|
||||
$fragments[$statement->name->value] = $statement;
|
||||
}
|
||||
}
|
||||
$this->fragments = $fragments;
|
||||
}
|
||||
return isset($fragments[$name]) ? $fragments[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HasSelectionSet $node
|
||||
* @return FragmentSpreadNode[]
|
||||
*/
|
||||
function getFragmentSpreads(HasSelectionSet $node)
|
||||
{
|
||||
$spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null;
|
||||
if (!$spreads) {
|
||||
$spreads = [];
|
||||
$setsToVisit = [$node->selectionSet];
|
||||
while (!empty($setsToVisit)) {
|
||||
$set = array_pop($setsToVisit);
|
||||
|
||||
for ($i = 0; $i < count($set->selections); $i++) {
|
||||
$selection = $set->selections[$i];
|
||||
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
|
||||
$spreads[] = $selection;
|
||||
} else if ($selection->selectionSet) {
|
||||
$setsToVisit[] = $selection->selectionSet;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->fragmentSpreads[$node] = $spreads;
|
||||
}
|
||||
return $spreads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OperationDefinitionNode $operation
|
||||
* @return FragmentDefinitionNode[]
|
||||
*/
|
||||
function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
|
||||
{
|
||||
$fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null;
|
||||
|
||||
if (!$fragments) {
|
||||
$fragments = [];
|
||||
$collectedNames = [];
|
||||
$nodesToVisit = [$operation];
|
||||
while (!empty($nodesToVisit)) {
|
||||
$node = array_pop($nodesToVisit);
|
||||
$spreads = $this->getFragmentSpreads($node);
|
||||
for ($i = 0; $i < count($spreads); $i++) {
|
||||
$fragName = $spreads[$i]->name->value;
|
||||
|
||||
if (empty($collectedNames[$fragName])) {
|
||||
$collectedNames[$fragName] = true;
|
||||
$fragment = $this->getFragment($fragName);
|
||||
if ($fragment) {
|
||||
$fragments[] = $fragment;
|
||||
$nodesToVisit[] = $fragment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->recursivelyReferencedFragments[$operation] = $fragments;
|
||||
}
|
||||
return $fragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HasSelectionSet $node
|
||||
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
|
||||
*/
|
||||
function getVariableUsages(HasSelectionSet $node)
|
||||
{
|
||||
$usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null;
|
||||
|
||||
if (!$usages) {
|
||||
$newUsages = [];
|
||||
$typeInfo = new TypeInfo($this->schema);
|
||||
Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [
|
||||
NodeKind::VARIABLE_DEFINITION => function () {
|
||||
return false;
|
||||
},
|
||||
NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) {
|
||||
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
|
||||
}
|
||||
]));
|
||||
$usages = $newUsages;
|
||||
$this->variableUsages[$node] = $usages;
|
||||
}
|
||||
return $usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OperationDefinitionNode $operation
|
||||
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
|
||||
*/
|
||||
function getRecursiveVariableUsages(OperationDefinitionNode $operation)
|
||||
{
|
||||
$usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null;
|
||||
|
||||
if (!$usages) {
|
||||
$usages = $this->getVariableUsages($operation);
|
||||
$fragments = $this->getRecursivelyReferencedFragments($operation);
|
||||
|
||||
$tmp = [$usages];
|
||||
for ($i = 0; $i < count($fragments); $i++) {
|
||||
$tmp[] = $this->getVariableUsages($fragments[$i]);
|
||||
}
|
||||
$usages = call_user_func_array('array_merge', $tmp);
|
||||
$this->recursiveVariableUsages[$operation] = $usages;
|
||||
}
|
||||
return $usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OutputType
|
||||
*
|
||||
* @return Type
|
||||
*/
|
||||
function getType()
|
||||
{
|
||||
return $this->typeInfo->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompositeType
|
||||
*/
|
||||
function getParentType()
|
||||
{
|
||||
return $this->typeInfo->getParentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
function getInputType()
|
||||
{
|
||||
return $this->typeInfo->getInputType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
function getFieldDef()
|
||||
{
|
||||
return $this->typeInfo->getFieldDef();
|
||||
}
|
||||
|
||||
function getDirective()
|
||||
{
|
||||
return $this->typeInfo->getDirective();
|
||||
}
|
||||
|
||||
function getArgument()
|
||||
{
|
||||
return $this->typeInfo->getArgument();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user