Vendor
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
use GraphQL\Validator\Rules\ExecutableDefinitions;
|
||||
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
||||
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNames;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
|
||||
use GraphQL\Validator\Rules\KnownDirectives;
|
||||
use GraphQL\Validator\Rules\KnownFragmentNames;
|
||||
use GraphQL\Validator\Rules\KnownTypeNames;
|
||||
use GraphQL\Validator\Rules\LoneAnonymousOperation;
|
||||
use GraphQL\Validator\Rules\LoneSchemaDefinition;
|
||||
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\ProvidedRequiredArgumentsOnDirectives;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use GraphQL\Validator\Rules\QuerySecurityRule;
|
||||
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\ValidationRule;
|
||||
use GraphQL\Validator\Rules\ValuesOfCorrectType;
|
||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
|
||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||
use Throwable;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* 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\ValidationRule
|
||||
* 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
|
||||
{
|
||||
/** @var ValidationRule[] */
|
||||
private static $rules = [];
|
||||
|
||||
/** @var ValidationRule[]|null */
|
||||
private static $defaultRules;
|
||||
|
||||
/** @var QuerySecurityRule[]|null */
|
||||
private static $securityRules;
|
||||
|
||||
/** @var ValidationRule[]|null */
|
||||
private static $sdlRules;
|
||||
|
||||
/** @var bool */
|
||||
private static $initRules = false;
|
||||
|
||||
/**
|
||||
* Primary method for query validation. See class description for details.
|
||||
*
|
||||
* @param ValidationRule[]|null $rules
|
||||
*
|
||||
* @return Error[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function validate(
|
||||
Schema $schema,
|
||||
DocumentNode $ast,
|
||||
?array $rules = null,
|
||||
?TypeInfo $typeInfo = null
|
||||
) {
|
||||
if ($rules === null) {
|
||||
$rules = static::allRules();
|
||||
}
|
||||
|
||||
if (is_array($rules) === true && count($rules) === 0) {
|
||||
// Skip validation if there are no rules
|
||||
return [];
|
||||
}
|
||||
|
||||
$typeInfo = $typeInfo ?: new TypeInfo($schema);
|
||||
|
||||
return static::visitUsingRules($schema, $typeInfo, $ast, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all global validation rules.
|
||||
*
|
||||
* @return ValidationRule[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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 (self::$defaultRules === null) {
|
||||
self::$defaultRules = [
|
||||
ExecutableDefinitions::class => new ExecutableDefinitions(),
|
||||
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(),
|
||||
ValuesOfCorrectType::class => new ValuesOfCorrectType(),
|
||||
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
|
||||
VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
|
||||
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
|
||||
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
|
||||
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
|
||||
];
|
||||
}
|
||||
|
||||
return self::$defaultRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QuerySecurityRule[]
|
||||
*/
|
||||
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 (self::$securityRules === null) {
|
||||
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;
|
||||
}
|
||||
|
||||
public static function sdlRules()
|
||||
{
|
||||
if (self::$sdlRules === null) {
|
||||
self::$sdlRules = [
|
||||
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
|
||||
KnownDirectives::class => new KnownDirectives(),
|
||||
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
|
||||
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
|
||||
UniqueArgumentNames::class => new UniqueArgumentNames(),
|
||||
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
|
||||
ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
|
||||
];
|
||||
}
|
||||
|
||||
return self::$sdlRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses a specialized visitor which runs multiple visitors in parallel,
|
||||
* while maintaining the visitor skip and break API.
|
||||
*
|
||||
* @param ValidationRule[] $rules
|
||||
*
|
||||
* @return Error[]
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return ValidationRule
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getRule($name)
|
||||
{
|
||||
$rules = static::allRules();
|
||||
|
||||
if (isset($rules[$name])) {
|
||||
return $rules[$name];
|
||||
}
|
||||
|
||||
$name = sprintf('GraphQL\\Validator\\Rules\\%s', $name);
|
||||
|
||||
return $rules[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rule to list of global validation rules
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function addRule(ValidationRule $rule)
|
||||
{
|
||||
self::$rules[$rule->getName()] = $rule;
|
||||
}
|
||||
|
||||
public static function isError($value)
|
||||
{
|
||||
return is_array($value)
|
||||
? count(array_filter(
|
||||
$value,
|
||||
static 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 which determines if a value literal node is valid for an input type.
|
||||
*
|
||||
* Deprecated. Rely on validation for documents co
|
||||
* ntaining literal values.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @return Error[]
|
||||
*/
|
||||
public static function isValidLiteralValue(Type $type, $valueNode)
|
||||
{
|
||||
$emptySchema = new Schema([]);
|
||||
$emptyDoc = new DocumentNode(['definitions' => []]);
|
||||
$typeInfo = new TypeInfo($emptySchema, $type);
|
||||
$context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
|
||||
$validator = new ValuesOfCorrectType();
|
||||
$visitor = $validator->getVisitor($context);
|
||||
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
|
||||
|
||||
return $context->getErrors();
|
||||
}
|
||||
|
||||
public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema)
|
||||
{
|
||||
$errors = self::visitUsingRules($schema, new TypeInfo($schema), $documentAST, self::sdlRules());
|
||||
if (count($errors) !== 0) {
|
||||
throw new Error(
|
||||
implode(
|
||||
"\n\n",
|
||||
array_map(static function (Error $error) : string {
|
||||
return $error->message;
|
||||
}, $errors)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class CustomValidationRule extends ValidationRule
|
||||
{
|
||||
/** @var callable */
|
||||
private $visitorFn;
|
||||
|
||||
public function __construct($name, callable $visitorFn)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->visitorFn = $visitorFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$fn = $this->visitorFn;
|
||||
|
||||
return $fn($context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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 QuerySecurityRule
|
||||
{
|
||||
public const ENABLED = 1;
|
||||
|
||||
/** @var bool */
|
||||
private $isEnabled;
|
||||
|
||||
public function __construct($enabled = self::ENABLED)
|
||||
{
|
||||
$this->setEnabled($enabled);
|
||||
}
|
||||
|
||||
public function setEnabled($enabled)
|
||||
{
|
||||
$this->isEnabled = $enabled;
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return $this->invokeIfNeeded(
|
||||
$context,
|
||||
[
|
||||
NodeKind::FIELD => static function (FieldNode $node) use ($context) {
|
||||
if ($node->name->value !== '__type' && $node->name->value !== '__schema') {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
static::introspectionDisabledMessage(),
|
||||
[$node]
|
||||
));
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function introspectionDisabledMessage()
|
||||
{
|
||||
return 'GraphQL introspection is not allowed, but the query contained __schema or __type';
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->isEnabled !== self::DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Executable definitions
|
||||
*
|
||||
* A GraphQL document is only valid for execution if all definitions are either
|
||||
* operation or fragment definitions.
|
||||
*/
|
||||
class ExecutableDefinitions extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context) {
|
||||
/** @var Node $definition */
|
||||
foreach ($node->definitions as $definition) {
|
||||
if ($definition instanceof OperationDefinitionNode ||
|
||||
$definition instanceof FragmentDefinitionNode
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::nonExecutableDefinitionMessage($definition->name->value),
|
||||
[$definition->name]
|
||||
));
|
||||
}
|
||||
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function nonExecutableDefinitionMessage($defName)
|
||||
{
|
||||
return sprintf('The "%s" definition is not executable.', $defName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function arsort;
|
||||
use function sprintf;
|
||||
|
||||
class FieldsOnCorrectType extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => function (FieldNode $node) use ($context) {
|
||||
$type = $context->getParentType();
|
||||
if (! $type) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldDef = $context->getFieldDef();
|
||||
if ($fieldDef) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This isn't valid. Let's find suggestions, if any.
|
||||
$schema = $context->getSchema();
|
||||
$fieldName = $node->name->value;
|
||||
// First determine if there are any suggested types to condition on.
|
||||
$suggestedTypeNames = $this->getSuggestedTypeNames(
|
||||
$schema,
|
||||
$type,
|
||||
$fieldName
|
||||
);
|
||||
// If there are no suggested types, then perhaps this was a typo?
|
||||
$suggestedFieldNames = $suggestedTypeNames
|
||||
? []
|
||||
: $this->getSuggestedFieldNames(
|
||||
$schema,
|
||||
$type,
|
||||
$fieldName
|
||||
);
|
||||
|
||||
// Report an error, including helpful suggestions.
|
||||
$context->reportError(new Error(
|
||||
static::undefinedFieldMessage(
|
||||
$node->name->value,
|
||||
$type->name,
|
||||
$suggestedTypeNames,
|
||||
$suggestedFieldNames
|
||||
),
|
||||
[$node]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through all of the implementations of type, as well as the interfaces
|
||||
* that they implement. If any of those types include the provided field,
|
||||
* suggest them, sorted by how often the type is referenced, starting
|
||||
* with Interfaces.
|
||||
*
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getSuggestedTypeNames(Schema $schema, $type, $fieldName)
|
||||
{
|
||||
if (Type::isAbstractType($type)) {
|
||||
$suggestedObjectTypes = [];
|
||||
$interfaceUsageCount = [];
|
||||
|
||||
foreach ($schema->getPossibleTypes($type) as $possibleType) {
|
||||
$fields = $possibleType->getFields();
|
||||
if (! isset($fields[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
// This object type defines this field.
|
||||
$suggestedObjectTypes[] = $possibleType->name;
|
||||
foreach ($possibleType->getInterfaces() as $possibleInterface) {
|
||||
$fields = $possibleInterface->getFields();
|
||||
if (! isset($fields[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
// This interface type defines this field.
|
||||
$interfaceUsageCount[$possibleInterface->name] =
|
||||
! isset($interfaceUsageCount[$possibleInterface->name])
|
||||
? 0
|
||||
: $interfaceUsageCount[$possibleInterface->name] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest interface types based on how common they are.
|
||||
arsort($interfaceUsageCount);
|
||||
$suggestedInterfaceTypes = array_keys($interfaceUsageCount);
|
||||
|
||||
// Suggest both interface and object types.
|
||||
return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
|
||||
}
|
||||
|
||||
// Otherwise, must be an Object type, which does not have possible fields.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* For the field name provided, determine if there are any similar field names
|
||||
* that may be the result of a typo.
|
||||
*
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return array|string[]
|
||||
*/
|
||||
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
|
||||
{
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$possibleFieldNames = array_keys($type->getFields());
|
||||
|
||||
return Utils::suggestionList($fieldName, $possibleFieldNames);
|
||||
}
|
||||
|
||||
// Otherwise, must be a Union type, which does not define fields.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fieldName
|
||||
* @param string $type
|
||||
* @param string[] $suggestedTypeNames
|
||||
* @param string[] $suggestedFieldNames
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function undefinedFieldMessage(
|
||||
$fieldName,
|
||||
$type,
|
||||
array $suggestedTypeNames,
|
||||
array $suggestedFieldNames
|
||||
) {
|
||||
$message = sprintf('Cannot query field "%s" on type "%s".', $fieldName, $type);
|
||||
|
||||
if ($suggestedTypeNames) {
|
||||
$suggestions = Utils::quotedOrList($suggestedTypeNames);
|
||||
|
||||
$message .= sprintf(' Did you mean to use an inline fragment on %s?', $suggestions);
|
||||
} elseif (! empty($suggestedFieldNames)) {
|
||||
$suggestions = Utils::quotedOrList($suggestedFieldNames);
|
||||
|
||||
$message .= sprintf(' Did you mean %s?', $suggestions);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function sprintf;
|
||||
|
||||
class FragmentsOnCompositeTypes extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context) {
|
||||
if (! $node->typeCondition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
|
||||
if (! $type || Type::isCompositeType($type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
static::inlineFragmentOnNonCompositeErrorMessage($type),
|
||||
[$node->typeCondition]
|
||||
));
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context) {
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
|
||||
|
||||
if (! $type || Type::isCompositeType($type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
static::fragmentOnNonCompositeErrorMessage(
|
||||
$node->name->value,
|
||||
Printer::doPrint($node->typeCondition)
|
||||
),
|
||||
[$node->typeCondition]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function inlineFragmentOnNonCompositeErrorMessage($type)
|
||||
{
|
||||
return sprintf('Fragment cannot condition on non composite type "%s".', $type);
|
||||
}
|
||||
|
||||
public static function fragmentOnNonCompositeErrorMessage($fragName, $type)
|
||||
{
|
||||
return sprintf('Fragment "%s" cannot condition on non composite type "%s".', $fragName, $type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\AST\NodeList;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Known argument names
|
||||
*
|
||||
* A GraphQL field is only valid if all supplied arguments are defined by
|
||||
* that field.
|
||||
*/
|
||||
class KnownArgumentNames extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::ARGUMENT => static function (ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
|
||||
/** @var NodeList|Node[] $ancestors */
|
||||
$argDef = $context->getArgument();
|
||||
if ($argDef !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$argumentOf = $ancestors[count($ancestors) - 1];
|
||||
if ($argumentOf->kind === NodeKind::FIELD) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
$parentType = $context->getParentType();
|
||||
if ($fieldDef && $parentType) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownArgMessage(
|
||||
$node->name->value,
|
||||
$fieldDef->name,
|
||||
$parentType->name,
|
||||
Utils::suggestionList(
|
||||
$node->name->value,
|
||||
array_map(
|
||||
static function ($arg) {
|
||||
return $arg->name;
|
||||
},
|
||||
$fieldDef->args
|
||||
)
|
||||
)
|
||||
),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
} elseif ($argumentOf->kind === NodeKind::DIRECTIVE) {
|
||||
$directive = $context->getDirective();
|
||||
if ($directive) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownDirectiveArgMessage(
|
||||
$node->name->value,
|
||||
$directive->name,
|
||||
Utils::suggestionList(
|
||||
$node->name->value,
|
||||
array_map(
|
||||
static function ($arg) {
|
||||
return $arg->name;
|
||||
},
|
||||
$directive->args
|
||||
)
|
||||
)
|
||||
),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $suggestedArgs
|
||||
*/
|
||||
public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs)
|
||||
{
|
||||
$message = sprintf('Unknown argument "%s" on field "%s" of type "%s".', $argName, $fieldName, $typeName);
|
||||
if (! empty($suggestedArgs)) {
|
||||
$message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs));
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $suggestedArgs
|
||||
*/
|
||||
public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs)
|
||||
{
|
||||
$message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName);
|
||||
if (! empty($suggestedArgs)) {
|
||||
$message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs));
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_map;
|
||||
use function in_array;
|
||||
use function iterator_to_array;
|
||||
|
||||
/**
|
||||
* Known argument names on directives
|
||||
*
|
||||
* A GraphQL directive is only valid if all supplied arguments are defined by
|
||||
* that field.
|
||||
*/
|
||||
class KnownArgumentNamesOnDirectives extends ValidationRule
|
||||
{
|
||||
protected static function unknownDirectiveArgMessage(string $argName, string $directionName)
|
||||
{
|
||||
return 'Unknown argument "' . $argName . '" on directive "@' . $directionName . '".';
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$directiveArgs = [];
|
||||
$schema = $context->getSchema();
|
||||
$definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives();
|
||||
|
||||
foreach ($definedDirectives as $directive) {
|
||||
$directiveArgs[$directive->name] = array_map(
|
||||
static function (FieldArgument $arg) : string {
|
||||
return $arg->name;
|
||||
},
|
||||
$directive->args
|
||||
);
|
||||
}
|
||||
|
||||
$astDefinitions = $context->getDocument()->definitions;
|
||||
foreach ($astDefinitions as $def) {
|
||||
if (! ($def instanceof DirectiveDefinitionNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $def->name->value;
|
||||
if ($def->arguments !== null) {
|
||||
$arguments = $def->arguments;
|
||||
|
||||
if ($arguments instanceof NodeList) {
|
||||
$arguments = iterator_to_array($arguments->getIterator());
|
||||
}
|
||||
|
||||
$directiveArgs[$name] = array_map(static function (InputValueDefinitionNode $arg) : string {
|
||||
return $arg->name->value;
|
||||
}, $arguments);
|
||||
} else {
|
||||
$directiveArgs[$name] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) {
|
||||
$directiveName = $directiveNode->name->value;
|
||||
$knownArgs = $directiveArgs[$directiveName] ?? null;
|
||||
|
||||
if ($directiveNode->arguments === null || ! $knownArgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($directiveNode->arguments as $argNode) {
|
||||
$argName = $argNode->name->value;
|
||||
if (in_array($argName, $knownArgs, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::unknownDirectiveArgMessage($argName, $directiveName),
|
||||
[$argNode]
|
||||
));
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\DirectiveLocation;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function sprintf;
|
||||
|
||||
class KnownDirectives extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$locationsMap = [];
|
||||
$schema = $context->getSchema();
|
||||
$definedDirectives = $schema->getDirectives();
|
||||
|
||||
foreach ($definedDirectives as $directive) {
|
||||
$locationsMap[$directive->name] = $directive->locations;
|
||||
}
|
||||
|
||||
$astDefinition = $context->getDocument()->definitions;
|
||||
|
||||
foreach ($astDefinition as $def) {
|
||||
if (! ($def instanceof DirectiveDefinitionNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$locationsMap[$def->name->value] = array_map(
|
||||
static function ($name) {
|
||||
return $name->value;
|
||||
},
|
||||
$def->locations
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
NodeKind::DIRECTIVE => function (
|
||||
DirectiveNode $node,
|
||||
$key,
|
||||
$parent,
|
||||
$path,
|
||||
$ancestors
|
||||
) use (
|
||||
$context,
|
||||
$locationsMap
|
||||
) {
|
||||
$name = $node->name->value;
|
||||
$locations = $locationsMap[$name] ?? null;
|
||||
|
||||
if (! $locations) {
|
||||
$context->reportError(new Error(
|
||||
self::unknownDirectiveMessage($name),
|
||||
[$node]
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
|
||||
|
||||
if (! $candidateLocation || in_array($candidateLocation, $locations, true)) {
|
||||
return;
|
||||
}
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::misplacedDirectiveMessage($name, $candidateLocation),
|
||||
[$node]
|
||||
)
|
||||
);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function unknownDirectiveMessage($directiveName)
|
||||
{
|
||||
return sprintf('Unknown directive "%s".', $directiveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[]|NodeList[] $ancestors The type is actually (Node|NodeList)[] but this PSR-5 syntax is so far not supported by most of the tools
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getDirectiveLocationForASTPath(array $ancestors)
|
||||
{
|
||||
$appliedTo = $ancestors[count($ancestors) - 1];
|
||||
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;
|
||||
case NodeKind::SCHEMA_DEFINITION:
|
||||
case NodeKind::SCHEMA_EXTENSION:
|
||||
return DirectiveLocation::SCHEMA;
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
case NodeKind::SCALAR_TYPE_EXTENSION:
|
||||
return DirectiveLocation::SCALAR;
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
case NodeKind::OBJECT_TYPE_EXTENSION:
|
||||
return DirectiveLocation::OBJECT;
|
||||
case NodeKind::FIELD_DEFINITION:
|
||||
return DirectiveLocation::FIELD_DEFINITION;
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
case NodeKind::INTERFACE_TYPE_EXTENSION:
|
||||
return DirectiveLocation::IFACE;
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
case NodeKind::UNION_TYPE_EXTENSION:
|
||||
return DirectiveLocation::UNION;
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
case NodeKind::ENUM_TYPE_EXTENSION:
|
||||
return DirectiveLocation::ENUM;
|
||||
case NodeKind::ENUM_VALUE_DEFINITION:
|
||||
return DirectiveLocation::ENUM_VALUE;
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
|
||||
return DirectiveLocation::INPUT_OBJECT;
|
||||
case NodeKind::INPUT_VALUE_DEFINITION:
|
||||
$parentNode = $ancestors[count($ancestors) - 3];
|
||||
|
||||
return $parentNode instanceof InputObjectTypeDefinitionNode
|
||||
? DirectiveLocation::INPUT_FIELD_DEFINITION
|
||||
: DirectiveLocation::ARGUMENT_DEFINITION;
|
||||
}
|
||||
}
|
||||
|
||||
public static function misplacedDirectiveMessage($directiveName, $location)
|
||||
{
|
||||
return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class KnownFragmentNames extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
$fragment = $context->getFragment($fragmentName);
|
||||
if ($fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::unknownFragmentMessage($fragmentName),
|
||||
[$node->name]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fragName
|
||||
*/
|
||||
public static function unknownFragmentMessage($fragName)
|
||||
{
|
||||
return sprintf('Unknown fragment "%s".', $fragName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_keys;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Known type names
|
||||
*
|
||||
* A GraphQL document is only valid if referenced types (specifically
|
||||
* variable definitions and fragment conditions) are defined by the type schema.
|
||||
*/
|
||||
class KnownTypeNames extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$skip = static function () {
|
||||
return Visitor::skipNode();
|
||||
};
|
||||
|
||||
return [
|
||||
// TODO: when validating IDL, re-enable these. Experimental version does not
|
||||
// add unreferenced types, resulting in false-positive errors. Squelched
|
||||
// errors for now.
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
|
||||
NodeKind::UNION_TYPE_DEFINITION => $skip,
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
|
||||
NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context) {
|
||||
$schema = $context->getSchema();
|
||||
$typeName = $node->name->value;
|
||||
$type = $schema->getType($typeName);
|
||||
if ($type !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::unknownTypeMessage(
|
||||
$typeName,
|
||||
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
|
||||
),
|
||||
[$node]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string[] $suggestedTypes
|
||||
*/
|
||||
public static function unknownTypeMessage($type, array $suggestedTypes)
|
||||
{
|
||||
$message = sprintf('Unknown type "%s".', $type);
|
||||
if (! empty($suggestedTypes)) {
|
||||
$suggestions = Utils::quotedOrList($suggestedTypes);
|
||||
|
||||
$message .= sprintf(' Did you mean %s?', $suggestions);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* 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 ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$operationCount = 0;
|
||||
|
||||
return [
|
||||
NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount) {
|
||||
$tmp = Utils::filter(
|
||||
$node->definitions,
|
||||
static function (Node $definition) {
|
||||
return $definition->kind === NodeKind::OPERATION_DEFINITION;
|
||||
}
|
||||
);
|
||||
|
||||
$operationCount = count($tmp);
|
||||
},
|
||||
NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (
|
||||
&$operationCount,
|
||||
$context
|
||||
) {
|
||||
if ($node->name || $operationCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(self::anonOperationNotAloneMessage(), [$node])
|
||||
);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function anonOperationNotAloneMessage()
|
||||
{
|
||||
return 'This anonymous operation must be the only defined operation.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
/**
|
||||
* Lone Schema definition
|
||||
*
|
||||
* A GraphQL document is only valid if it contains only one schema definition.
|
||||
*/
|
||||
class LoneSchemaDefinition extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$oldSchema = $context->getSchema();
|
||||
$alreadyDefined = $oldSchema !== null ? (
|
||||
$oldSchema->getAstNode() ||
|
||||
$oldSchema->getQueryType() ||
|
||||
$oldSchema->getMutationType() ||
|
||||
$oldSchema->getSubscriptionType()
|
||||
) : false;
|
||||
|
||||
$schemaDefinitionsCount = 0;
|
||||
|
||||
return [
|
||||
NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) {
|
||||
if ($alreadyDefined !== false) {
|
||||
$context->reportError(new Error('Cannot define a new schema within a schema extension.', $node));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($schemaDefinitionsCount > 0) {
|
||||
$context->reportError(new Error('Must provide only one schema definition.', $node));
|
||||
}
|
||||
|
||||
++$schemaDefinitionsCount;
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_slice;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
|
||||
class NoFragmentCycles extends ValidationRule
|
||||
{
|
||||
/** @var bool[] */
|
||||
public $visitedFrags;
|
||||
|
||||
/** @var FragmentSpreadNode[] */
|
||||
public $spreadPath;
|
||||
|
||||
/** @var (int|null)[] */
|
||||
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 => static 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 = $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,
|
||||
static function ($s) {
|
||||
return $s->name->value;
|
||||
}
|
||||
)
|
||||
),
|
||||
$nodes
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->spreadPathIndexByName[$fragmentName] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $spreadNames
|
||||
*/
|
||||
public static function cycleErrorMessage($fragName, array $spreadNames = [])
|
||||
{
|
||||
return sprintf(
|
||||
'Cannot spread fragment "%s" within itself%s.',
|
||||
$fragName,
|
||||
! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : ''
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* A GraphQL operation is only valid if all variables encountered, both directly
|
||||
* and via fragment spreads, are defined by that operation.
|
||||
*/
|
||||
class NoUndefinedVariables extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$variableNameDefined = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => [
|
||||
'enter' => static function () use (&$variableNameDefined) {
|
||||
$variableNameDefined = [];
|
||||
},
|
||||
'leave' => static 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])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::undefinedVarMessage(
|
||||
$varName,
|
||||
$operation->name ? $operation->name->value : null
|
||||
),
|
||||
[$node, $operation]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined) {
|
||||
$variableNameDefined[$def->variable->name->value] = true;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function undefinedVarMessage($varName, $opName = null)
|
||||
{
|
||||
return $opName
|
||||
? sprintf('Variable "$%s" is not defined by operation "%s".', $varName, $opName)
|
||||
: sprintf('Variable "$%s" is not defined.', $varName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class NoUnusedFragments extends ValidationRule
|
||||
{
|
||||
/** @var OperationDefinitionNode[] */
|
||||
public $operationDefs;
|
||||
|
||||
/** @var FragmentDefinitionNode[] */
|
||||
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])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::unusedFragMessage($fragName),
|
||||
[$fragmentDef]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function unusedFragMessage($fragName)
|
||||
{
|
||||
return sprintf('Fragment "%s" is never used.', $fragName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function sprintf;
|
||||
|
||||
class NoUnusedVariables extends ValidationRule
|
||||
{
|
||||
/** @var VariableDefinitionNode[] */
|
||||
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])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::unusedVariableMessage($variableName, $opName),
|
||||
[$variableDef]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => function ($def) {
|
||||
$this->variableDefs[] = $def;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function unusedVariableMessage($varName, $opName = null)
|
||||
{
|
||||
return $opName
|
||||
? sprintf('Variable "$%s" is never used in operation "%s".', $varName, $opName)
|
||||
: sprintf('Variable "$%s" is never used.', $varName);
|
||||
}
|
||||
}
|
||||
+900
@@ -0,0 +1,900 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
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\Language\Printer;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
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\PairSet;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use SplObjectStorage;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
|
||||
class OverlappingFieldsCanBeMerged extends ValidationRule
|
||||
{
|
||||
/**
|
||||
* A memoization for when two fragments are compared "between" each other for
|
||||
* conflicts. Two fragments may be compared many times, so memoizing this can
|
||||
* dramatically improve the performance of this validator.
|
||||
*
|
||||
* @var PairSet
|
||||
*/
|
||||
private $comparedFragmentPairs;
|
||||
|
||||
/**
|
||||
* A cache for the "field map" and list of fragment names found in any given
|
||||
* selection set. Selection sets may be asked for this information multiple
|
||||
* times, so this improves the performance of this validator.
|
||||
*
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $cachedFieldsAndFragmentNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->comparedFragmentPairs = new PairSet();
|
||||
$this->cachedFieldsAndFragmentNames = new SplObjectStorage();
|
||||
|
||||
return [
|
||||
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) {
|
||||
$conflicts = $this->findConflictsWithinSelectionSet(
|
||||
$context,
|
||||
$context->getParentType(),
|
||||
$selectionSet
|
||||
);
|
||||
|
||||
foreach ($conflicts as $conflict) {
|
||||
[[$responseName, $reason], $fields1, $fields2] = $conflict;
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::fieldsConflictMessage($responseName, $reason),
|
||||
array_merge($fields1, $fields2)
|
||||
));
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all conflicts found "within" a selection set, including those found
|
||||
* via spreading in fragments. Called when visiting each SelectionSet in the
|
||||
* GraphQL Document.
|
||||
*
|
||||
* @param CompositeType $parentType
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function findConflictsWithinSelectionSet(
|
||||
ValidationContext $context,
|
||||
$parentType,
|
||||
SelectionSetNode $selectionSet
|
||||
) {
|
||||
[$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames(
|
||||
$context,
|
||||
$parentType,
|
||||
$selectionSet
|
||||
);
|
||||
|
||||
$conflicts = [];
|
||||
|
||||
// (A) Find find all conflicts "within" the fields of this selection set.
|
||||
// Note: this is the *only place* `collectConflictsWithin` is called.
|
||||
$this->collectConflictsWithin(
|
||||
$context,
|
||||
$conflicts,
|
||||
$fieldMap
|
||||
);
|
||||
|
||||
$fragmentNamesLength = count($fragmentNames);
|
||||
if ($fragmentNamesLength !== 0) {
|
||||
// (B) Then collect conflicts between these fields and those represented by
|
||||
// each spread fragment name found.
|
||||
$comparedFragments = [];
|
||||
for ($i = 0; $i < $fragmentNamesLength; $i++) {
|
||||
$this->collectConflictsBetweenFieldsAndFragment(
|
||||
$context,
|
||||
$conflicts,
|
||||
$comparedFragments,
|
||||
false,
|
||||
$fieldMap,
|
||||
$fragmentNames[$i]
|
||||
);
|
||||
// (C) Then compare this fragment with all other fragments found in this
|
||||
// selection set to collect conflicts between fragments spread together.
|
||||
// This compares each item in the list of fragment names to every other item
|
||||
// in that same list (except for itself).
|
||||
for ($j = $i + 1; $j < $fragmentNamesLength; $j++) {
|
||||
$this->collectConflictsBetweenFragments(
|
||||
$context,
|
||||
$conflicts,
|
||||
false,
|
||||
$fragmentNames[$i],
|
||||
$fragmentNames[$j]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a selection set, return the collection of fields (a mapping of response
|
||||
* name to field ASTs and definitions) as well as a list of fragment names
|
||||
* referenced via fragment spreads.
|
||||
*
|
||||
* @param CompositeType $parentType
|
||||
*
|
||||
* @return mixed[]|SplObjectStorage
|
||||
*/
|
||||
private function getFieldsAndFragmentNames(
|
||||
ValidationContext $context,
|
||||
$parentType,
|
||||
SelectionSetNode $selectionSet
|
||||
) {
|
||||
if (isset($this->cachedFieldsAndFragmentNames[$selectionSet])) {
|
||||
$cached = $this->cachedFieldsAndFragmentNames[$selectionSet];
|
||||
} else {
|
||||
$astAndDefs = [];
|
||||
$fragmentNames = [];
|
||||
|
||||
$this->internalCollectFieldsAndFragmentNames(
|
||||
$context,
|
||||
$parentType,
|
||||
$selectionSet,
|
||||
$astAndDefs,
|
||||
$fragmentNames
|
||||
);
|
||||
$cached = [$astAndDefs, array_keys($fragmentNames)];
|
||||
$this->cachedFieldsAndFragmentNames[$selectionSet] = $cached;
|
||||
}
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm:
|
||||
*
|
||||
* Conflicts occur when two fields exist in a query which will produce the same
|
||||
* response name, but represent differing values, thus creating a conflict.
|
||||
* The algorithm below finds all conflicts via making a series of comparisons
|
||||
* between fields. In order to compare as few fields as possible, this makes
|
||||
* a series of comparisons "within" sets of fields and "between" sets of fields.
|
||||
*
|
||||
* Given any selection set, a collection produces both a set of fields by
|
||||
* also including all inline fragments, as well as a list of fragments
|
||||
* referenced by fragment spreads.
|
||||
*
|
||||
* A) Each selection set represented in the document first compares "within" its
|
||||
* collected set of fields, finding any conflicts between every pair of
|
||||
* overlapping fields.
|
||||
* Note: This is the *only time* that a the fields "within" a set are compared
|
||||
* to each other. After this only fields "between" sets are compared.
|
||||
*
|
||||
* B) Also, if any fragment is referenced in a selection set, then a
|
||||
* comparison is made "between" the original set of fields and the
|
||||
* referenced fragment.
|
||||
*
|
||||
* C) Also, if multiple fragments are referenced, then comparisons
|
||||
* are made "between" each referenced fragment.
|
||||
*
|
||||
* D) When comparing "between" a set of fields and a referenced fragment, first
|
||||
* a comparison is made between each field in the original set of fields and
|
||||
* each field in the the referenced set of fields.
|
||||
*
|
||||
* E) Also, if any fragment is referenced in the referenced selection set,
|
||||
* then a comparison is made "between" the original set of fields and the
|
||||
* referenced fragment (recursively referring to step D).
|
||||
*
|
||||
* F) When comparing "between" two fragments, first a comparison is made between
|
||||
* each field in the first referenced set of fields and each field in the the
|
||||
* second referenced set of fields.
|
||||
*
|
||||
* G) Also, any fragments referenced by the first must be compared to the
|
||||
* second, and any fragments referenced by the second must be compared to the
|
||||
* first (recursively referring to step F).
|
||||
*
|
||||
* H) When comparing two fields, if both have selection sets, then a comparison
|
||||
* is made "between" both selection sets, first comparing the set of fields in
|
||||
* the first selection set with the set of fields in the second.
|
||||
*
|
||||
* I) Also, if any fragment is referenced in either selection set, then a
|
||||
* comparison is made "between" the other set of fields and the
|
||||
* referenced fragment.
|
||||
*
|
||||
* J) Also, if two fragments are referenced in both selection sets, then a
|
||||
* comparison is made "between" the two fragments.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Given a reference to a fragment, return the represented collection of fields
|
||||
* as well as a list of nested fragment names referenced via fragment spreads.
|
||||
*
|
||||
* @param CompositeType $parentType
|
||||
* @param mixed[][][] $astAndDefs
|
||||
* @param bool[] $fragmentNames
|
||||
*/
|
||||
private function internalCollectFieldsAndFragmentNames(
|
||||
ValidationContext $context,
|
||||
$parentType,
|
||||
SelectionSetNode $selectionSet,
|
||||
array &$astAndDefs,
|
||||
array &$fragmentNames
|
||||
) {
|
||||
foreach ($selectionSet->selections as $selection) {
|
||||
switch (true) {
|
||||
case $selection instanceof FieldNode:
|
||||
$fieldName = $selection->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType instanceof ObjectType ||
|
||||
$parentType instanceof InterfaceType) {
|
||||
$tmp = $parentType->getFields();
|
||||
if (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
}
|
||||
}
|
||||
$responseName = $selection->alias ? $selection->alias->value : $fieldName;
|
||||
|
||||
if (! isset($astAndDefs[$responseName])) {
|
||||
$astAndDefs[$responseName] = [];
|
||||
}
|
||||
$astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
|
||||
break;
|
||||
case $selection instanceof FragmentSpreadNode:
|
||||
$fragmentNames[$selection->name->value] = true;
|
||||
break;
|
||||
case $selection instanceof InlineFragmentNode:
|
||||
$typeCondition = $selection->typeCondition;
|
||||
$inlineFragmentType = $typeCondition
|
||||
? TypeInfo::typeFromAST($context->getSchema(), $typeCondition)
|
||||
: $parentType;
|
||||
|
||||
$this->internalCollectFieldsAndFragmentNames(
|
||||
$context,
|
||||
$inlineFragmentType,
|
||||
$selection->selectionSet,
|
||||
$astAndDefs,
|
||||
$fragmentNames
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all Conflicts "within" one collection of fields.
|
||||
*
|
||||
* @param mixed[][] $conflicts
|
||||
* @param mixed[][] $fieldMap
|
||||
*/
|
||||
private function collectConflictsWithin(
|
||||
ValidationContext $context,
|
||||
array &$conflicts,
|
||||
array $fieldMap
|
||||
) {
|
||||
// A field map is a keyed collection, where each key represents a response
|
||||
// name and the value at that key is a list of all fields which provide that
|
||||
// response name. For every response name, if there are multiple fields, they
|
||||
// must be compared to find a potential conflict.
|
||||
foreach ($fieldMap as $responseName => $fields) {
|
||||
// This compares every field in the list to every other field in this list
|
||||
// (except to itself). If the list only has one item, nothing needs to
|
||||
// be compared.
|
||||
$fieldsLength = count($fields);
|
||||
if ($fieldsLength <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $fieldsLength; $i++) {
|
||||
for ($j = $i + 1; $j < $fieldsLength; $j++) {
|
||||
$conflict = $this->findConflict(
|
||||
$context,
|
||||
false, // within one collection is never mutually exclusive
|
||||
$responseName,
|
||||
$fields[$i],
|
||||
$fields[$j]
|
||||
);
|
||||
if (! $conflict) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$conflicts[] = $conflict;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if there is a conflict between two particular fields, including
|
||||
* comparing their sub-fields.
|
||||
*
|
||||
* @param bool $parentFieldsAreMutuallyExclusive
|
||||
* @param string $responseName
|
||||
* @param mixed[] $field1
|
||||
* @param mixed[] $field2
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
private function findConflict(
|
||||
ValidationContext $context,
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
array $field1,
|
||||
array $field2
|
||||
) {
|
||||
[$parentType1, $ast1, $def1] = $field1;
|
||||
[$parentType2, $ast2, $def2] = $field2;
|
||||
|
||||
// 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.
|
||||
$areMutuallyExclusive =
|
||||
$parentFieldsAreMutuallyExclusive ||
|
||||
(
|
||||
$parentType1 !== $parentType2 &&
|
||||
$parentType1 instanceof ObjectType &&
|
||||
$parentType2 instanceof ObjectType
|
||||
);
|
||||
|
||||
// The return type for each field.
|
||||
$type1 = $def1 === null ? null : $def1->getType();
|
||||
$type2 = $def2 === null ? null : $def2->getType();
|
||||
|
||||
if (! $areMutuallyExclusive) {
|
||||
// Two aliases must refer to the same field.
|
||||
$name1 = $ast1->name->value;
|
||||
$name2 = $ast2->name->value;
|
||||
if ($name1 !== $name2) {
|
||||
return [
|
||||
[$responseName, sprintf('%s and %s are different fields', $name1, $name2)],
|
||||
[$ast1],
|
||||
[$ast2],
|
||||
];
|
||||
}
|
||||
|
||||
if (! $this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) {
|
||||
return [
|
||||
[$responseName, 'they have differing arguments'],
|
||||
[$ast1],
|
||||
[$ast2],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) {
|
||||
return [
|
||||
[$responseName, sprintf('they return conflicting types %s and %s', $type1, $type2)],
|
||||
[$ast1],
|
||||
[$ast2],
|
||||
];
|
||||
}
|
||||
|
||||
// Collect and compare sub-fields. Use the same "visited fragment names" list
|
||||
// for both collections so fields in a fragment reference are never
|
||||
// compared to themselves.
|
||||
$selectionSet1 = $ast1->selectionSet;
|
||||
$selectionSet2 = $ast2->selectionSet;
|
||||
if ($selectionSet1 && $selectionSet2) {
|
||||
$conflicts = $this->findConflictsBetweenSubSelectionSets(
|
||||
$context,
|
||||
$areMutuallyExclusive,
|
||||
Type::getNamedType($type1),
|
||||
$selectionSet1,
|
||||
Type::getNamedType($type2),
|
||||
$selectionSet2
|
||||
);
|
||||
|
||||
return $this->subfieldConflicts(
|
||||
$conflicts,
|
||||
$responseName,
|
||||
$ast1,
|
||||
$ast2
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArgumentNode[] $arguments1
|
||||
* @param ArgumentNode[] $arguments2
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function sameArguments($arguments1, $arguments2)
|
||||
{
|
||||
if (count($arguments1) !== count($arguments2)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($arguments1 as $argument1) {
|
||||
$argument2 = null;
|
||||
foreach ($arguments2 as $argument) {
|
||||
if ($argument->name->value === $argument1->name->value) {
|
||||
$argument2 = $argument;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $argument2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->sameValue($argument1->value, $argument2->value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function sameValue(Node $value1, Node $value2)
|
||||
{
|
||||
return (! $value1 && ! $value2) || (Printer::doPrint($value1) === Printer::doPrint($value2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Two types conflict if both types could not apply to a value simultaneously.
|
||||
* Composite types are ignored as their individual field types will be compared
|
||||
* later recursively. However List and Non-Null types must match.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all conflicts found between two selection sets, including those found
|
||||
* via spreading in fragments. Called when determining if conflicts exist
|
||||
* between the sub-fields of two overlapping fields.
|
||||
*
|
||||
* @param bool $areMutuallyExclusive
|
||||
* @param CompositeType $parentType1
|
||||
* @param CompositeType $parentType2
|
||||
*
|
||||
* @return mixed[][]
|
||||
*/
|
||||
private function findConflictsBetweenSubSelectionSets(
|
||||
ValidationContext $context,
|
||||
$areMutuallyExclusive,
|
||||
$parentType1,
|
||||
SelectionSetNode $selectionSet1,
|
||||
$parentType2,
|
||||
SelectionSetNode $selectionSet2
|
||||
) {
|
||||
$conflicts = [];
|
||||
|
||||
[$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames(
|
||||
$context,
|
||||
$parentType1,
|
||||
$selectionSet1
|
||||
);
|
||||
[$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames(
|
||||
$context,
|
||||
$parentType2,
|
||||
$selectionSet2
|
||||
);
|
||||
|
||||
// (H) First, collect all conflicts between these two collections of field.
|
||||
$this->collectConflictsBetween(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap1,
|
||||
$fieldMap2
|
||||
);
|
||||
|
||||
// (I) Then collect conflicts between the first collection of fields and
|
||||
// those referenced by each fragment name associated with the second.
|
||||
$fragmentNames2Length = count($fragmentNames2);
|
||||
if ($fragmentNames2Length !== 0) {
|
||||
$comparedFragments = [];
|
||||
for ($j = 0; $j < $fragmentNames2Length; $j++) {
|
||||
$this->collectConflictsBetweenFieldsAndFragment(
|
||||
$context,
|
||||
$conflicts,
|
||||
$comparedFragments,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap1,
|
||||
$fragmentNames2[$j]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// (I) Then collect conflicts between the second collection of fields and
|
||||
// those referenced by each fragment name associated with the first.
|
||||
$fragmentNames1Length = count($fragmentNames1);
|
||||
if ($fragmentNames1Length !== 0) {
|
||||
$comparedFragments = [];
|
||||
for ($i = 0; $i < $fragmentNames1Length; $i++) {
|
||||
$this->collectConflictsBetweenFieldsAndFragment(
|
||||
$context,
|
||||
$conflicts,
|
||||
$comparedFragments,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap2,
|
||||
$fragmentNames1[$i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// (J) Also collect conflicts between any fragment names by the first and
|
||||
// fragment names by the second. This compares each item in the first set of
|
||||
// names to each item in the second set of names.
|
||||
for ($i = 0; $i < $fragmentNames1Length; $i++) {
|
||||
for ($j = 0; $j < $fragmentNames2Length; $j++) {
|
||||
$this->collectConflictsBetweenFragments(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fragmentNames1[$i],
|
||||
$fragmentNames2[$j]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all Conflicts between two collections of fields. This is similar to,
|
||||
* but different from the `collectConflictsWithin` function above. This check
|
||||
* assumes that `collectConflictsWithin` has already been called on each
|
||||
* provided collection of fields. This is true because this validator traverses
|
||||
* each individual selection set.
|
||||
*
|
||||
* @param mixed[][] $conflicts
|
||||
* @param bool $parentFieldsAreMutuallyExclusive
|
||||
* @param mixed[] $fieldMap1
|
||||
* @param mixed[] $fieldMap2
|
||||
*/
|
||||
private function collectConflictsBetween(
|
||||
ValidationContext $context,
|
||||
array &$conflicts,
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
array $fieldMap1,
|
||||
array $fieldMap2
|
||||
) {
|
||||
// A field map is a keyed collection, where each key represents a response
|
||||
// name and the value at that key is a list of all fields which provide that
|
||||
// response name. For any response name which appears in both provided field
|
||||
// maps, each field from the first field map must be compared to every field
|
||||
// in the second field map to find potential conflicts.
|
||||
foreach ($fieldMap1 as $responseName => $fields1) {
|
||||
if (! isset($fieldMap2[$responseName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields2 = $fieldMap2[$responseName];
|
||||
$fields1Length = count($fields1);
|
||||
$fields2Length = count($fields2);
|
||||
for ($i = 0; $i < $fields1Length; $i++) {
|
||||
for ($j = 0; $j < $fields2Length; $j++) {
|
||||
$conflict = $this->findConflict(
|
||||
$context,
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
$fields1[$i],
|
||||
$fields2[$j]
|
||||
);
|
||||
if (! $conflict) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$conflicts[] = $conflict;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all conflicts found between a set of fields and a fragment reference
|
||||
* including via spreading in any nested fragments.
|
||||
*
|
||||
* @param mixed[][] $conflicts
|
||||
* @param bool[] $comparedFragments
|
||||
* @param bool $areMutuallyExclusive
|
||||
* @param mixed[][] $fieldMap
|
||||
* @param string $fragmentName
|
||||
*/
|
||||
private function collectConflictsBetweenFieldsAndFragment(
|
||||
ValidationContext $context,
|
||||
array &$conflicts,
|
||||
array &$comparedFragments,
|
||||
$areMutuallyExclusive,
|
||||
array $fieldMap,
|
||||
$fragmentName
|
||||
) {
|
||||
if (isset($comparedFragments[$fragmentName])) {
|
||||
return;
|
||||
}
|
||||
$comparedFragments[$fragmentName] = true;
|
||||
|
||||
$fragment = $context->getFragment($fragmentName);
|
||||
if (! $fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
|
||||
$context,
|
||||
$fragment
|
||||
);
|
||||
|
||||
if ($fieldMap === $fieldMap2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// (D) First collect any conflicts between the provided collection of fields
|
||||
// and the collection of fields represented by the given fragment.
|
||||
$this->collectConflictsBetween(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap,
|
||||
$fieldMap2
|
||||
);
|
||||
|
||||
// (E) Then collect any conflicts between the provided collection of fields
|
||||
// and any fragment names found in the given fragment.
|
||||
$fragmentNames2Length = count($fragmentNames2);
|
||||
for ($i = 0; $i < $fragmentNames2Length; $i++) {
|
||||
$this->collectConflictsBetweenFieldsAndFragment(
|
||||
$context,
|
||||
$conflicts,
|
||||
$comparedFragments,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap,
|
||||
$fragmentNames2[$i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a reference to a fragment, return the represented collection of fields
|
||||
* as well as a list of nested fragment names referenced via fragment spreads.
|
||||
*
|
||||
* @return mixed[]|SplObjectStorage
|
||||
*/
|
||||
private function getReferencedFieldsAndFragmentNames(
|
||||
ValidationContext $context,
|
||||
FragmentDefinitionNode $fragment
|
||||
) {
|
||||
// Short-circuit building a type from the AST if possible.
|
||||
if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) {
|
||||
return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet];
|
||||
}
|
||||
|
||||
$fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition);
|
||||
|
||||
return $this->getFieldsAndFragmentNames(
|
||||
$context,
|
||||
$fragmentType,
|
||||
$fragment->selectionSet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all conflicts found between two fragments, including via spreading in
|
||||
* any nested fragments.
|
||||
*
|
||||
* @param mixed[][] $conflicts
|
||||
* @param bool $areMutuallyExclusive
|
||||
* @param string $fragmentName1
|
||||
* @param string $fragmentName2
|
||||
*/
|
||||
private function collectConflictsBetweenFragments(
|
||||
ValidationContext $context,
|
||||
array &$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fragmentName1,
|
||||
$fragmentName2
|
||||
) {
|
||||
// No need to compare a fragment to itself.
|
||||
if ($fragmentName1 === $fragmentName2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Memoize so two fragments are not compared for conflicts more than once.
|
||||
if ($this->comparedFragmentPairs->has(
|
||||
$fragmentName1,
|
||||
$fragmentName2,
|
||||
$areMutuallyExclusive
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$this->comparedFragmentPairs->add(
|
||||
$fragmentName1,
|
||||
$fragmentName2,
|
||||
$areMutuallyExclusive
|
||||
);
|
||||
|
||||
$fragment1 = $context->getFragment($fragmentName1);
|
||||
$fragment2 = $context->getFragment($fragmentName2);
|
||||
if (! $fragment1 || ! $fragment2) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames(
|
||||
$context,
|
||||
$fragment1
|
||||
);
|
||||
[$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
|
||||
$context,
|
||||
$fragment2
|
||||
);
|
||||
|
||||
// (F) First, collect all conflicts between these two collections of fields
|
||||
// (not including any nested fragments).
|
||||
$this->collectConflictsBetween(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fieldMap1,
|
||||
$fieldMap2
|
||||
);
|
||||
|
||||
// (G) Then collect conflicts between the first fragment and any nested
|
||||
// fragments spread in the second fragment.
|
||||
$fragmentNames2Length = count($fragmentNames2);
|
||||
for ($j = 0; $j < $fragmentNames2Length; $j++) {
|
||||
$this->collectConflictsBetweenFragments(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fragmentName1,
|
||||
$fragmentNames2[$j]
|
||||
);
|
||||
}
|
||||
|
||||
// (G) Then collect conflicts between the second fragment and any nested
|
||||
// fragments spread in the first fragment.
|
||||
$fragmentNames1Length = count($fragmentNames1);
|
||||
for ($i = 0; $i < $fragmentNames1Length; $i++) {
|
||||
$this->collectConflictsBetweenFragments(
|
||||
$context,
|
||||
$conflicts,
|
||||
$areMutuallyExclusive,
|
||||
$fragmentNames1[$i],
|
||||
$fragmentName2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a series of Conflicts which occurred between two sub-fields, generate
|
||||
* a single Conflict.
|
||||
*
|
||||
* @param mixed[][] $conflicts
|
||||
* @param string $responseName
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
private function subfieldConflicts(
|
||||
array $conflicts,
|
||||
$responseName,
|
||||
FieldNode $ast1,
|
||||
FieldNode $ast2
|
||||
) {
|
||||
if (count($conflicts) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
$responseName,
|
||||
array_map(
|
||||
static function ($conflict) {
|
||||
return $conflict[0];
|
||||
},
|
||||
$conflicts
|
||||
),
|
||||
],
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
static function ($allFields, $conflict) {
|
||||
return array_merge($allFields, $conflict[1]);
|
||||
},
|
||||
[$ast1]
|
||||
),
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
static function ($allFields, $conflict) {
|
||||
return array_merge($allFields, $conflict[2]);
|
||||
},
|
||||
[$ast2]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $responseName
|
||||
* @param string $reason
|
||||
*/
|
||||
public static function fieldsConflictMessage($responseName, $reason)
|
||||
{
|
||||
$reasonMessage = self::reasonMessage($reason);
|
||||
|
||||
return sprintf(
|
||||
'Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.',
|
||||
$responseName,
|
||||
$reasonMessage
|
||||
);
|
||||
}
|
||||
|
||||
public static function reasonMessage($reason)
|
||||
{
|
||||
if (is_array($reason)) {
|
||||
$tmp = array_map(
|
||||
static function ($tmp) {
|
||||
[$responseName, $subReason] = $tmp;
|
||||
|
||||
$reasonMessage = self::reasonMessage($subReason);
|
||||
|
||||
return sprintf('subfields "%s" conflict because %s', $responseName, $reasonMessage);
|
||||
},
|
||||
$reason
|
||||
);
|
||||
|
||||
return implode(' and ', $tmp);
|
||||
}
|
||||
|
||||
return $reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class PossibleFragmentSpreads extends ValidationRule
|
||||
{
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
|
||||
[$node]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
|
||||
{
|
||||
return sprintf(
|
||||
'Fragment cannot be spread here as objects of type "%s" can never be of type "%s".',
|
||||
$parentType,
|
||||
$fragType
|
||||
);
|
||||
}
|
||||
|
||||
private function getFragmentType(ValidationContext $context, $name)
|
||||
{
|
||||
$frag = $context->getFragment($name);
|
||||
if ($frag) {
|
||||
$type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition);
|
||||
if ($type instanceof CompositeType) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
|
||||
{
|
||||
return sprintf(
|
||||
'Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".',
|
||||
$fragName,
|
||||
$parentType,
|
||||
$fragType
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class ProvidedNonNullArguments extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => [
|
||||
'leave' => static 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 = $argNodeMap[$argDef->name] ?? null;
|
||||
if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
|
||||
[$fieldNode]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
NodeKind::DIRECTIVE => [
|
||||
'leave' => static 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 = $argNodeMap[$argDef->name] ?? null;
|
||||
if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::missingDirectiveArgMessage(
|
||||
$directiveNode->name->value,
|
||||
$argDef->name,
|
||||
$argDef->getType()
|
||||
),
|
||||
[$directiveNode]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function missingFieldArgMessage($fieldName, $argName, $type)
|
||||
{
|
||||
return sprintf(
|
||||
'Field "%s" argument "%s" of type "%s" is required but not provided.',
|
||||
$fieldName,
|
||||
$argName,
|
||||
$type
|
||||
);
|
||||
}
|
||||
|
||||
public static function missingDirectiveArgMessage($directiveName, $argName, $type)
|
||||
{
|
||||
return sprintf(
|
||||
'Directive "@%s" argument "%s" of type "%s" is required but not provided.',
|
||||
$directiveName,
|
||||
$argName,
|
||||
$type
|
||||
);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\AST\NonNullTypeNode;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_filter;
|
||||
use function is_array;
|
||||
use function iterator_to_array;
|
||||
|
||||
/**
|
||||
* Provided required arguments on directives
|
||||
*
|
||||
* A directive is only valid if all required (non-null without a
|
||||
* default value) field arguments have been provided.
|
||||
*/
|
||||
class ProvidedRequiredArgumentsOnDirectives extends ValidationRule
|
||||
{
|
||||
protected static function missingDirectiveArgMessage(string $directiveName, string $argName)
|
||||
{
|
||||
return 'Directive "' . $directiveName . '" argument "' . $argName . '" is required but ont provided.';
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$requiredArgsMap = [];
|
||||
$schema = $context->getSchema();
|
||||
$definedDirectives = $schema->getDirectives();
|
||||
|
||||
foreach ($definedDirectives as $directive) {
|
||||
$requiredArgsMap[$directive->name] = Utils::keyMap(
|
||||
array_filter($directive->args, static function (FieldArgument $arg) : bool {
|
||||
return $arg->getType() instanceof NonNull && ! isset($arg->defaultValue);
|
||||
}),
|
||||
static function (FieldArgument $arg) : string {
|
||||
return $arg->name;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$astDefinition = $context->getDocument()->definitions;
|
||||
foreach ($astDefinition as $def) {
|
||||
if (! ($def instanceof DirectiveDefinitionNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($def->arguments)) {
|
||||
$arguments = $def->arguments;
|
||||
} elseif ($def->arguments instanceof NodeList) {
|
||||
$arguments = iterator_to_array($def->arguments->getIterator());
|
||||
} else {
|
||||
$arguments = null;
|
||||
}
|
||||
|
||||
$requiredArgsMap[$def->name->value] = Utils::keyMap(
|
||||
$arguments ? array_filter($arguments, static function (Node $argument) : bool {
|
||||
return $argument instanceof NonNullTypeNode &&
|
||||
(
|
||||
! isset($argument->defaultValue) ||
|
||||
$argument->defaultValue === null
|
||||
);
|
||||
}) : [],
|
||||
static function (NamedTypeNode $argument) : string {
|
||||
return $argument->name->value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context) {
|
||||
$directiveName = $directiveNode->name->value;
|
||||
$requiredArgs = $requiredArgsMap[$directiveName] ?? null;
|
||||
if (! $requiredArgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
$argNodes = $directiveNode->arguments ?: [];
|
||||
$argNodeMap = Utils::keyMap(
|
||||
$argNodes,
|
||||
static function (ArgumentNode $arg) : string {
|
||||
return $arg->name->value;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($requiredArgs as $argName => $arg) {
|
||||
if (isset($argNodeMap[$argName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(static::missingDirectiveArgMessage($directiveName, $argName), [$directiveNode])
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use ArrayObject;
|
||||
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;
|
||||
use function array_map;
|
||||
use function call_user_func_array;
|
||||
use function implode;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
|
||||
class QueryComplexity extends QuerySecurityRule
|
||||
{
|
||||
/** @var int */
|
||||
private $maxQueryComplexity;
|
||||
|
||||
/** @var mixed[]|null */
|
||||
private $rawVariableValues = [];
|
||||
|
||||
/** @var ArrayObject */
|
||||
private $variableDefs;
|
||||
|
||||
/** @var ArrayObject */
|
||||
private $fieldNodeAndDefs;
|
||||
|
||||
/** @var ValidationContext */
|
||||
private $context;
|
||||
|
||||
/** @var int */
|
||||
private $complexity;
|
||||
|
||||
public function __construct($maxQueryComplexity)
|
||||
{
|
||||
$this->setMaxQueryComplexity($maxQueryComplexity);
|
||||
}
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
$this->variableDefs = new ArrayObject();
|
||||
$this->fieldNodeAndDefs = new ArrayObject();
|
||||
$this->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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->complexity = $this->fieldComplexity($operationDefinition, $complexity);
|
||||
|
||||
if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(self::maxQueryComplexityErrorMessage(
|
||||
$this->getMaxQueryComplexity(),
|
||||
$this->getQueryComplexity()
|
||||
))
|
||||
);
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
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 ($fragment !== null) {
|
||||
$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 directiveExcludesField(FieldNode $node)
|
||||
{
|
||||
foreach ($node->directives as $directiveNode) {
|
||||
if ($directiveNode->name->value === 'deprecated') {
|
||||
return false;
|
||||
}
|
||||
[$errors, $variableValues] = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$this->getRawVariableValues()
|
||||
);
|
||||
if (! empty($errors)) {
|
||||
throw new Error(implode(
|
||||
"\n\n",
|
||||
array_map(
|
||||
static function ($error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$errors
|
||||
)
|
||||
));
|
||||
}
|
||||
if ($directiveNode->name->value === 'include') {
|
||||
$directive = Directive::includeDirective();
|
||||
/** @var bool $directiveArgsIf */
|
||||
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
|
||||
|
||||
return ! $directiveArgsIf;
|
||||
}
|
||||
$directive = Directive::skipDirective();
|
||||
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
||||
|
||||
return $directiveArgsIf['if'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getRawVariableValues()
|
||||
{
|
||||
return $this->rawVariableValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[]|null $rawVariableValues
|
||||
*/
|
||||
public function setRawVariableValues(?array $rawVariableValues = null)
|
||||
{
|
||||
$this->rawVariableValues = $rawVariableValues ?: [];
|
||||
}
|
||||
|
||||
private function buildFieldArguments(FieldNode $node)
|
||||
{
|
||||
$rawVariableValues = $this->getRawVariableValues();
|
||||
$astFieldInfo = $this->astFieldInfo($node);
|
||||
$fieldDef = $astFieldInfo[1];
|
||||
|
||||
$args = [];
|
||||
|
||||
if ($fieldDef instanceof FieldDefinition) {
|
||||
[$errors, $variableValues] = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$rawVariableValues
|
||||
);
|
||||
|
||||
if (! empty($errors)) {
|
||||
throw new Error(implode(
|
||||
"\n\n",
|
||||
array_map(
|
||||
static function ($error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$errors
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
public function getQueryComplexity()
|
||||
{
|
||||
return $this->complexity;
|
||||
}
|
||||
|
||||
public function getMaxQueryComplexity()
|
||||
{
|
||||
return $this->maxQueryComplexity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*/
|
||||
public function setMaxQueryComplexity($maxQueryComplexity)
|
||||
{
|
||||
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
|
||||
|
||||
$this->maxQueryComplexity = (int) $maxQueryComplexity;
|
||||
}
|
||||
|
||||
public static function maxQueryComplexityErrorMessage($max, $count)
|
||||
{
|
||||
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->getMaxQueryComplexity() !== self::DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\AST\SelectionSetNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class QueryDepth extends QuerySecurityRule
|
||||
{
|
||||
/** @var int */
|
||||
private $maxQueryDepth;
|
||||
|
||||
public function __construct($maxQueryDepth)
|
||||
{
|
||||
$this->setMaxQueryDepth($maxQueryDepth);
|
||||
}
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(self::maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
|
||||
);
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
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 ($node->selectionSet !== null) {
|
||||
// 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 ($node->selectionSet !== null) {
|
||||
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
|
||||
}
|
||||
break;
|
||||
|
||||
case NodeKind::FRAGMENT_SPREAD:
|
||||
/** @var FragmentSpreadNode $node */
|
||||
$fragment = $this->getFragment($node);
|
||||
|
||||
if ($fragment !== null) {
|
||||
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $maxDepth;
|
||||
}
|
||||
|
||||
public function getMaxQueryDepth()
|
||||
{
|
||||
return $this->maxQueryDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*/
|
||||
public function setMaxQueryDepth($maxQueryDepth)
|
||||
{
|
||||
$this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
|
||||
|
||||
$this->maxQueryDepth = (int) $maxQueryDepth;
|
||||
}
|
||||
|
||||
public static function maxQueryDepthErrorMessage($max, $count)
|
||||
{
|
||||
return sprintf('Max query depth should be %d but got %d.', $max, $count);
|
||||
}
|
||||
|
||||
protected function isEnabled()
|
||||
{
|
||||
return $this->getMaxQueryDepth() !== self::DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use ArrayObject;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
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;
|
||||
use InvalidArgumentException;
|
||||
use function class_alias;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
|
||||
abstract class QuerySecurityRule extends ValidationRule
|
||||
{
|
||||
public const DISABLED = 0;
|
||||
|
||||
/** @var FragmentDefinitionNode[] */
|
||||
private $fragments = [];
|
||||
|
||||
/**
|
||||
* check if equal to 0 no check is done. Must be greater or equal to 0.
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $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 getFragment(FragmentSpreadNode $fragmentSpread)
|
||||
{
|
||||
$spreadName = $fragmentSpread->name->value;
|
||||
$fragments = $this->getFragments();
|
||||
|
||||
return $fragments[$spreadName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FragmentDefinitionNode[]
|
||||
*/
|
||||
protected function getFragments()
|
||||
{
|
||||
return $this->fragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable[] $validators
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
protected function invokeIfNeeded(ValidationContext $context, array $validators)
|
||||
{
|
||||
// is disabled?
|
||||
if (! $this->isEnabled()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->gatherFragmentDefinition($context);
|
||||
|
||||
return $validators;
|
||||
}
|
||||
|
||||
abstract protected function isEnabled();
|
||||
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fragments[$node->name->value] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Type|null $parentType
|
||||
*
|
||||
* @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;
|
||||
|
||||
return $node->alias ? $node->alias->value : $fieldName;
|
||||
}
|
||||
}
|
||||
|
||||
class_alias(QuerySecurityRule::class, 'GraphQL\Validator\Rules\AbstractQuerySecurity');
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function sprintf;
|
||||
|
||||
class ScalarLeafs extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::FIELD => static function (FieldNode $node) use ($context) {
|
||||
$type = $context->getType();
|
||||
if (! $type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Type::isLeafType(Type::getNamedType($type))) {
|
||||
if ($node->selectionSet) {
|
||||
$context->reportError(new Error(
|
||||
self::noSubselectionAllowedMessage($node->name->value, $type),
|
||||
[$node->selectionSet]
|
||||
));
|
||||
}
|
||||
} elseif (! $node->selectionSet) {
|
||||
$context->reportError(new Error(
|
||||
self::requiredSubselectionMessage($node->name->value, $type),
|
||||
[$node]
|
||||
));
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function noSubselectionAllowedMessage($field, $type)
|
||||
{
|
||||
return sprintf('Field "%s" of type "%s" must not have a sub selection.', $field, $type);
|
||||
}
|
||||
|
||||
public static function requiredSubselectionMessage($field, $type)
|
||||
{
|
||||
return sprintf('Field "%s" of type "%s" must have a sub selection.', $field, $type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueArgumentNames extends ValidationRule
|
||||
{
|
||||
/** @var NameNode[] */
|
||||
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();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateArgMessage($argName)
|
||||
{
|
||||
return sprintf('There can be only one argument named "%s".', $argName);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueDirectivesPerLocation extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
'enter' => static function (Node $node) use ($context) {
|
||||
if (! isset($node->directives)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateDirectiveMessage($directiveName)
|
||||
{
|
||||
return sprintf('The directive "%s" can only be used once at this location.', $directiveName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueFragmentNames extends ValidationRule
|
||||
{
|
||||
/** @var NameNode[] */
|
||||
public $knownFragmentNames;
|
||||
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$this->knownFragmentNames = [];
|
||||
|
||||
return [
|
||||
NodeKind::OPERATION_DEFINITION => static function () {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
if (empty($this->knownFragmentNames[$fragmentName])) {
|
||||
$this->knownFragmentNames[$fragmentName] = $node->name;
|
||||
} else {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateFragmentNameMessage($fragmentName),
|
||||
[$this->knownFragmentNames[$fragmentName], $node->name]
|
||||
));
|
||||
}
|
||||
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateFragmentNameMessage($fragName)
|
||||
{
|
||||
return sprintf('There can be only one fragment named "%s".', $fragName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function array_pop;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueInputFieldNames extends ValidationRule
|
||||
{
|
||||
/** @var string[] */
|
||||
public $knownNames;
|
||||
|
||||
/** @var string[][] */
|
||||
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();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateInputFieldMessage($fieldName)
|
||||
{
|
||||
return sprintf('There can be only one input field named "%s".', $fieldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueOperationNames extends ValidationRule
|
||||
{
|
||||
/** @var NameNode[] */
|
||||
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])) {
|
||||
$this->knownOperationNames[$operationName->value] = $operationName;
|
||||
} else {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateOperationNameMessage($operationName->value),
|
||||
[$this->knownOperationNames[$operationName->value], $operationName]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => static function () {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateOperationNameMessage($operationName)
|
||||
{
|
||||
return sprintf('There can be only one operation named "%s".', $operationName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class UniqueVariableNames extends ValidationRule
|
||||
{
|
||||
/** @var NameNode[] */
|
||||
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])) {
|
||||
$this->knownVariableNames[$variableName] = $node->variable->name;
|
||||
} else {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateVariableMessage($variableName),
|
||||
[$this->knownVariableNames[$variableName], $node->variable->name]
|
||||
));
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function duplicateVariableMessage($variableName)
|
||||
{
|
||||
return sprintf('There can be only one variable named "%s".', $variableName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function class_alias;
|
||||
|
||||
abstract class ValidationRule
|
||||
{
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name ?: static::class;
|
||||
}
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return $this->getVisitor($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns structure suitable for GraphQL\Language\Visitor
|
||||
*
|
||||
* @see \GraphQL\Language\Visitor
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
abstract public function getVisitor(ValidationContext $context);
|
||||
}
|
||||
|
||||
class_alias(ValidationRule::class, 'GraphQL\Validator\Rules\AbstractValidationRule');
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NullValueNode;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\AST\ObjectValueNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use Throwable;
|
||||
use function array_combine;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function iterator_to_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Value literals of correct type
|
||||
*
|
||||
* A GraphQL document is only valid if all value literals are of the type
|
||||
* expected at their position.
|
||||
*/
|
||||
class ValuesOfCorrectType extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
$fieldName = '';
|
||||
|
||||
return [
|
||||
NodeKind::FIELD => [
|
||||
'enter' => static function (FieldNode $node) use (&$fieldName) {
|
||||
$fieldName = $node->name->value;
|
||||
},
|
||||
],
|
||||
NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName) {
|
||||
$type = $context->getInputType();
|
||||
if (! ($type instanceof NonNull)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::getBadValueMessage((string) $type, Printer::doPrint($node), null, $context, $fieldName),
|
||||
$node
|
||||
)
|
||||
);
|
||||
},
|
||||
NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName) {
|
||||
// Note: TypeInfo will traverse into a list's item type, so look to the
|
||||
// parent input type to check if it is a list.
|
||||
$type = Type::getNullableType($context->getParentInputType());
|
||||
if (! $type instanceof ListOfType) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
},
|
||||
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) {
|
||||
// Note: TypeInfo will traverse into a list's item type, so look to the
|
||||
// parent input type to check if it is a list.
|
||||
$type = Type::getNamedType($context->getInputType());
|
||||
if (! $type instanceof InputObjectType) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
unset($fieldName);
|
||||
// Ensure every required field exists.
|
||||
$inputFields = $type->getFields();
|
||||
$nodeFields = iterator_to_array($node->fields);
|
||||
$fieldNodeMap = array_combine(
|
||||
array_map(
|
||||
static function ($field) {
|
||||
return $field->name->value;
|
||||
},
|
||||
$nodeFields
|
||||
),
|
||||
array_values($nodeFields)
|
||||
);
|
||||
foreach ($inputFields as $fieldName => $fieldDef) {
|
||||
$fieldType = $fieldDef->getType();
|
||||
if (isset($fieldNodeMap[$fieldName]) || ! ($fieldType instanceof NonNull)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
|
||||
$node
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) {
|
||||
$parentType = Type::getNamedType($context->getParentInputType());
|
||||
$fieldType = $context->getInputType();
|
||||
if ($fieldType || ! ($parentType instanceof InputObjectType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$suggestions = Utils::suggestionList(
|
||||
$node->name->value,
|
||||
array_keys($parentType->getFields())
|
||||
);
|
||||
$didYouMean = $suggestions
|
||||
? 'Did you mean ' . Utils::orList($suggestions) . '?'
|
||||
: null;
|
||||
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean),
|
||||
$node
|
||||
)
|
||||
);
|
||||
},
|
||||
NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName) {
|
||||
$type = Type::getNamedType($context->getInputType());
|
||||
if (! $type instanceof EnumType) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
} elseif (! $type->getValue($node->value)) {
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::getBadValueMessage(
|
||||
$type->name,
|
||||
Printer::doPrint($node),
|
||||
$this->enumTypeSuggestion($type, $node),
|
||||
$context,
|
||||
$fieldName
|
||||
),
|
||||
$node
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
},
|
||||
NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
},
|
||||
NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
},
|
||||
NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName) {
|
||||
$this->isValidScalar($context, $node, $fieldName);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function badValueMessage($typeName, $valueName, $message = null)
|
||||
{
|
||||
return sprintf('Expected type %s, found %s', $typeName, $valueName) .
|
||||
($message ? "; ${message}" : '.');
|
||||
}
|
||||
|
||||
private function isValidScalar(ValidationContext $context, ValueNode $node, $fieldName)
|
||||
{
|
||||
// Report any error at the full type expected by the location.
|
||||
$locationType = $context->getInputType();
|
||||
|
||||
if (! $locationType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = Type::getNamedType($locationType);
|
||||
|
||||
if (! $type instanceof ScalarType) {
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::getBadValueMessage(
|
||||
(string) $locationType,
|
||||
Printer::doPrint($node),
|
||||
$this->enumTypeSuggestion($type, $node),
|
||||
$context,
|
||||
$fieldName
|
||||
),
|
||||
$node
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Scalars determine if a literal value is valid via parseLiteral() which
|
||||
// may throw to indicate failure.
|
||||
try {
|
||||
$type->parseLiteral($node);
|
||||
} catch (Exception $error) {
|
||||
// Ensure a reference to the original error is maintained.
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::getBadValueMessage(
|
||||
(string) $locationType,
|
||||
Printer::doPrint($node),
|
||||
$error->getMessage(),
|
||||
$context,
|
||||
$fieldName
|
||||
),
|
||||
$node
|
||||
)
|
||||
);
|
||||
} catch (Throwable $error) {
|
||||
// Ensure a reference to the original error is maintained.
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::getBadValueMessage(
|
||||
(string) $locationType,
|
||||
Printer::doPrint($node),
|
||||
$error->getMessage(),
|
||||
$context,
|
||||
$fieldName
|
||||
),
|
||||
$node
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function enumTypeSuggestion($type, ValueNode $node)
|
||||
{
|
||||
if ($type instanceof EnumType) {
|
||||
$suggestions = Utils::suggestionList(
|
||||
Printer::doPrint($node),
|
||||
array_map(
|
||||
static function (EnumValueDefinition $value) {
|
||||
return $value->name;
|
||||
},
|
||||
$type->getValues()
|
||||
)
|
||||
);
|
||||
|
||||
return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function badArgumentValueMessage($typeName, $valueName, $fieldName, $argName, $message = null)
|
||||
{
|
||||
return sprintf('Field "%s" argument "%s" requires type %s, found %s', $fieldName, $argName, $typeName, $valueName) .
|
||||
($message ? sprintf('; %s', $message) : '.');
|
||||
}
|
||||
|
||||
public static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
|
||||
{
|
||||
return sprintf('Field %s.%s of required type %s was not provided.', $typeName, $fieldName, $fieldTypeName);
|
||||
}
|
||||
|
||||
public static function unknownFieldMessage($typeName, $fieldName, $message = null)
|
||||
{
|
||||
return sprintf('Field "%s" is not defined by type %s', $fieldName, $typeName) .
|
||||
($message ? sprintf('; %s', $message) : '.');
|
||||
}
|
||||
|
||||
private static function getBadValueMessage($typeName, $valueName, $message = null, $context = null, $fieldName = null)
|
||||
{
|
||||
if ($context) {
|
||||
$arg = $context->getArgument();
|
||||
if ($arg) {
|
||||
return self::badArgumentValueMessage($typeName, $valueName, $fieldName, $arg->name, $message);
|
||||
}
|
||||
}
|
||||
|
||||
return self::badValueMessage($typeName, $valueName, $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
class VariablesAreInputTypes extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::VARIABLE_DEFINITION => static 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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variableName = $node->variable->name->value;
|
||||
$context->reportError(new Error(
|
||||
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
|
||||
[$node->type]
|
||||
));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function nonInputTypeOnVarMessage($variableName, $typeName)
|
||||
{
|
||||
return sprintf('Variable "$%s" cannot be non-input type "%s".', $variableName, $typeName);
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Variable's default value is allowed
|
||||
*
|
||||
* A GraphQL document is only valid if all variable default values are allowed
|
||||
* due to a variable not being required.
|
||||
*/
|
||||
class VariablesDefaultValueAllowed extends ValidationRule
|
||||
{
|
||||
public function getVisitor(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context) {
|
||||
$name = $node->variable->name->value;
|
||||
$defaultValue = $node->defaultValue;
|
||||
$type = $context->getInputType();
|
||||
if ($type instanceof NonNull && $defaultValue) {
|
||||
$context->reportError(
|
||||
new Error(
|
||||
self::defaultForRequiredVarMessage(
|
||||
$name,
|
||||
$type,
|
||||
$type->getWrappedType()
|
||||
),
|
||||
[$defaultValue]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::SELECTION_SET => static function (SelectionSetNode $node) {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static function defaultForRequiredVarMessage($varName, $type, $guessType)
|
||||
{
|
||||
return sprintf(
|
||||
'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".',
|
||||
$varName,
|
||||
$type,
|
||||
$guessType
|
||||
);
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use function sprintf;
|
||||
|
||||
class VariablesInAllowedPosition extends ValidationRule
|
||||
{
|
||||
/** @var */
|
||||
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 = $this->varDefMap[$varName] ?? null;
|
||||
|
||||
if ($varDef === null || $type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::badVarPosMessage($varName, $varType, $type),
|
||||
[$varDef, $node]
|
||||
));
|
||||
}
|
||||
},
|
||||
],
|
||||
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) {
|
||||
$this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private function effectiveType($varType, $varDef)
|
||||
{
|
||||
return ! $varDef->defaultValue || $varType instanceof NonNull ? $varType : new NonNull($varType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function badVarPosMessage($varName, $varType, $expectedType)
|
||||
{
|
||||
return sprintf(
|
||||
'Variable "$%s" of type "%s" used in position expecting type "%s".',
|
||||
$varName,
|
||||
$varType,
|
||||
$expectedType
|
||||
);
|
||||
}
|
||||
|
||||
/** If a variable definition has a default value, it's effectively non-null. */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\HasSelectionSet;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use SplObjectStorage;
|
||||
use function array_pop;
|
||||
use function call_user_func_array;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public 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();
|
||||
}
|
||||
|
||||
public function reportError(Error $error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
public function getSchema()
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
|
||||
*/
|
||||
public function getRecursiveVariableUsages(OperationDefinitionNode $operation)
|
||||
{
|
||||
$usages = $this->recursiveVariableUsages[$operation] ?? null;
|
||||
|
||||
if ($usages === null) {
|
||||
$usages = $this->getVariableUsages($operation);
|
||||
$fragments = $this->getRecursivelyReferencedFragments($operation);
|
||||
|
||||
$tmp = [$usages];
|
||||
foreach ($fragments as $i => $fragment) {
|
||||
$tmp[] = $this->getVariableUsages($fragments[$i]);
|
||||
}
|
||||
$usages = call_user_func_array('array_merge', $tmp);
|
||||
$this->recursiveVariableUsages[$operation] = $usages;
|
||||
}
|
||||
|
||||
return $usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
|
||||
*/
|
||||
private function getVariableUsages(HasSelectionSet $node)
|
||||
{
|
||||
$usages = $this->variableUsages[$node] ?? null;
|
||||
|
||||
if ($usages === null) {
|
||||
$newUsages = [];
|
||||
$typeInfo = new TypeInfo($this->schema);
|
||||
Visitor::visit(
|
||||
$node,
|
||||
Visitor::visitWithTypeInfo(
|
||||
$typeInfo,
|
||||
[
|
||||
NodeKind::VARIABLE_DEFINITION => static function () {
|
||||
return false;
|
||||
},
|
||||
NodeKind::VARIABLE => static function (VariableNode $variable) use (
|
||||
&$newUsages,
|
||||
$typeInfo
|
||||
) {
|
||||
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
$usages = $newUsages;
|
||||
$this->variableUsages[$node] = $usages;
|
||||
}
|
||||
|
||||
return $usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FragmentDefinitionNode[]
|
||||
*/
|
||||
public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
|
||||
{
|
||||
$fragments = $this->recursivelyReferencedFragments[$operation] ?? null;
|
||||
|
||||
if ($fragments === null) {
|
||||
$fragments = [];
|
||||
$collectedNames = [];
|
||||
$nodesToVisit = [$operation];
|
||||
while (! empty($nodesToVisit)) {
|
||||
$node = array_pop($nodesToVisit);
|
||||
$spreads = $this->getFragmentSpreads($node);
|
||||
foreach ($spreads as $spread) {
|
||||
$fragName = $spread->name->value;
|
||||
|
||||
if (! empty($collectedNames[$fragName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$collectedNames[$fragName] = true;
|
||||
$fragment = $this->getFragment($fragName);
|
||||
if (! $fragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fragments[] = $fragment;
|
||||
$nodesToVisit[] = $fragment;
|
||||
}
|
||||
}
|
||||
$this->recursivelyReferencedFragments[$operation] = $fragments;
|
||||
}
|
||||
|
||||
return $fragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FragmentSpreadNode[]
|
||||
*/
|
||||
public function getFragmentSpreads(HasSelectionSet $node)
|
||||
{
|
||||
$spreads = $this->fragmentSpreads[$node] ?? null;
|
||||
if ($spreads === null) {
|
||||
$spreads = [];
|
||||
/** @var SelectionSetNode[] $setsToVisit */
|
||||
$setsToVisit = [$node->selectionSet];
|
||||
while (! empty($setsToVisit)) {
|
||||
$set = array_pop($setsToVisit);
|
||||
|
||||
for ($i = 0, $selectionCount = count($set->selections); $i < $selectionCount; $i++) {
|
||||
$selection = $set->selections[$i];
|
||||
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
|
||||
$spreads[] = $selection;
|
||||
} elseif ($selection->selectionSet) {
|
||||
$setsToVisit[] = $selection->selectionSet;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->fragmentSpreads[$node] = $spreads;
|
||||
}
|
||||
|
||||
return $spreads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return FragmentDefinitionNode|null
|
||||
*/
|
||||
public function getFragment($name)
|
||||
{
|
||||
$fragments = $this->fragments;
|
||||
if (! $fragments) {
|
||||
$fragments = [];
|
||||
foreach ($this->getDocument()->definitions as $statement) {
|
||||
if ($statement->kind !== NodeKind::FRAGMENT_DEFINITION) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fragments[$statement->name->value] = $statement;
|
||||
}
|
||||
$this->fragments = $fragments;
|
||||
}
|
||||
|
||||
return $fragments[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DocumentNode
|
||||
*/
|
||||
public function getDocument()
|
||||
{
|
||||
return $this->ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OutputType
|
||||
*
|
||||
* @return Type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->typeInfo->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
public function getParentType()
|
||||
{
|
||||
return $this->typeInfo->getParentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
public function getInputType()
|
||||
{
|
||||
return $this->typeInfo->getInputType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
public function getParentInputType()
|
||||
{
|
||||
return $this->typeInfo->getParentInputType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
public function getFieldDef()
|
||||
{
|
||||
return $this->typeInfo->getFieldDef();
|
||||
}
|
||||
|
||||
public function getDirective()
|
||||
{
|
||||
return $this->typeInfo->getDirective();
|
||||
}
|
||||
|
||||
public function getArgument()
|
||||
{
|
||||
return $this->typeInfo->getArgument();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user