Repository navigation
Replies: 2 comments 2 replies
|
This could be done through a custom validation rule. It probably needs to be configured with pairs of fields that are the inverse of each other, since that does not necessarily follow from the schema definition. Consider the following example: "Represents a node in a tree, see https://en.wikipedia.org/wiki/Tree_(abstract_data_type)."
type TreeNode {
parent: TreeNode
children: [TreeNode!]!
siblings: [TreeNode!]!
}Considering the shape of a tree, |
|
Following up on @spawnia's suggestion, here's a working custom validation rule. It limits how many times a given <?php declare(strict_types=1);
use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Definition\HasFieldsType;
use GraphQL\Type\Definition\Type;
use GraphQL\Validator\QueryValidationContext;
use GraphQL\Validator\Rules\QuerySecurityRule;
final class FieldRepetitionLimit extends QuerySecurityRule
{
/** @param array<string, int> $limits "Type.field" => max occurrences on one path */
public function __construct(private array $limits) {}
public function getVisitor(QueryValidationContext $context): array
{
return $this->invokeIfNeeded($context, [
NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $operation) use ($context): void {
$rootType = $context->getSchema()->getOperationType($operation->operation);
if ($rootType !== null) {
$this->walk($context, $operation->selectionSet, $rootType, [], []);
}
},
]);
}
/**
* @param array<string, int> $seen coordinates used on the current path
* @param array<string, true> $fragments fragments entered on the current path
*/
private function walk(QueryValidationContext $context, SelectionSetNode $selectionSet, Type $parentType, array $seen, array $fragments): void
{
foreach ($selectionSet->selections as $selection) {
if ($selection instanceof FieldNode) {
if (! $parentType instanceof HasFieldsType || ! $parentType->hasField($selection->name->value)) {
continue; // unknown fields are reported by the FieldsOnCorrectType rule
}
$coordinate = $parentType->name . '.' . $selection->name->value;
$count = ($seen[$coordinate] ?? 0) + 1;
if (isset($this->limits[$coordinate]) && $count > $this->limits[$coordinate]) {
$context->reportError(new Error(
"Field \"{$coordinate}\" may appear at most {$this->limits[$coordinate]} time(s) on one path.",
[$selection]
));
continue;
}
if ($selection->selectionSet !== null) {
$fieldType = Type::getNamedType($parentType->getField($selection->name->value)->getType());
$this->walk($context, $selection->selectionSet, $fieldType, [$coordinate => $count] + $seen, $fragments);
}
} elseif ($selection instanceof InlineFragmentNode) {
$type = $selection->typeCondition !== null
? $context->getSchema()->getType($selection->typeCondition->name->value)
: $parentType;
if ($type !== null) {
$this->walk($context, $selection->selectionSet, $type, $seen, $fragments);
}
} elseif ($selection instanceof FragmentSpreadNode) {
$fragment = $this->getFragment($selection);
$name = $selection->name->value;
if ($fragment === null || isset($fragments[$name])) {
continue; // fragment cycles are reported by the NoFragmentCycles rule
}
$type = $context->getSchema()->getType($fragment->typeCondition->name->value);
if ($type !== null) {
$this->walk($context, $fragment->selectionSet, $type, $seen, $fragments + [$name => true]);
}
}
}
}
protected function isEnabled(): bool
{
return $this->limits !== [];
}
}Usage for your example: DocumentValidator::addRule(new FieldRepetitionLimit([
'Article.comments' => 1,
'Comment.article' => 1,
]));Results: Because the limits are per field and not a global depth, legitimate deep queries elsewhere in the schema are unaffected, and you can still keep a generous |

Uh oh!
There was an error while loading. Please reload this page.
Hi,
What are recommended practices if you have type tree which eventually can reference each other?
Assumme I've an
Articleand aCommentand I have queries likearticlesListandcommentsListandArticlehas comments but aCommentcan also reference it's article.Intended use cases:
or
It's now possible to also do this:
I'm aware of:
\GraphQL\Validator\Rules\QueryComplexityandsetMaxQueryComplexity\GraphQL\Validator\Rules\QueryDepthandsetMaxQueryDepthBut they need to be adjusted to their maxima I need for legitimate queries, of which I also have some, which needs higher values then for the example I gave.
I'm also aware with a proper data loader in place this shouldn't be a problem (n+1 problem), but I am looking at a different kind of protection.
Something which is looking at the types and is configurable to detect if e.g.
articleis already accessed in different levels in the tree and then thrown an error.A recommendations to have something similar to
QueryComplexityandQueryDepthbut more targeted?thanks!
All reactions