Skip to content

Commit

Permalink
Generalize validation for mutually exclusive arguments (#2233)
Browse files Browse the repository at this point in the history
  • Loading branch information
spawnia authored and bepsvpt committed Nov 21, 2022
1 parent 76e07f3 commit 3f9cc91
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 96 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

## v5.65.0

### Added

- Validate only one of any mutually exclusive directive arguments is defined https://github.com/nuwave/lighthouse/pull/2233

## v5.64.1

### Fixed
Expand Down
26 changes: 13 additions & 13 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ directive @aggregate(

"""
The relationship with the column to aggregate.
Mutually exclusive with the `model` argument.
Mutually exclusive with `model`.
"""
relation: String

"""
The model with the column to aggregate.
Mutually exclusive with the `relation` argument.
Mutually exclusive with `relation`.
"""
model: String

Expand Down Expand Up @@ -821,13 +821,13 @@ Returns the count of a given relationship or model.
directive @count(
"""
The relationship to count.
Mutually exclusive with the `model` argument.
Mutually exclusive with `model`.
"""
relation: String

"""
The model to count.
Mutually exclusive with the `relation` argument.
Mutually exclusive with `relation`.
"""
model: String

Expand Down Expand Up @@ -2255,15 +2255,15 @@ directive @node(
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
Mutually exclusive with the `model` argument.
Mutually exclusive with `model`.
"""
resolver: String

"""
Specify the class name of the model to use.
This is only needed when the default model detection does not work.
Mutually exclusive with the `model` argument.
Mutually exclusive with `resolver`.
"""
model: String
) on OBJECT
Expand Down Expand Up @@ -2334,15 +2334,15 @@ directive @orderBy(
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
Only used when the directive is added on an argument.
"""
columns: [String!]

"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
Only used when the directive is added on an argument.
"""
columnsEnum: String
Expand Down Expand Up @@ -2386,21 +2386,21 @@ Options for the `relations` argument on `@orderBy`.
"""
input OrderByRelation {
"""
TODO: description
Name of the relation.
"""
relation: String!

"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
"""
columns: [String!]

"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
"""
columnsEnum: String
}
Expand Down
16 changes: 8 additions & 8 deletions docs/master/eloquent/complex-where-conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ directive @whereConditions(
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
"""
columns: [String!]

"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
"""
columnsEnum: String

Expand Down Expand Up @@ -246,23 +246,23 @@ directive @whereHasConditions(
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
"""
columns: [String!]

"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
"""
columnsEnum: String

"""
Reference a method that applies the client given conditions to the query builder.
Expected signature: `(
\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder,
array<string, mixed> $whereConditions
\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder,
array<string, mixed> $whereConditions
): void`
Consists of two parts: a class name and a method name, separated by an `@` symbol.
Expand Down
16 changes: 1 addition & 15 deletions src/Auth/CanDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
use Nuwave\Lighthouse\Execution\Resolved;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
Expand Down Expand Up @@ -281,19 +280,6 @@ protected function buildCheckArguments(array $args): array

public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType)
{
$mutuallyExclusive = [
$this->directiveHasArgument('resolve'),
$this->directiveHasArgument('query'),
$this->directiveHasArgument('find'),
];

if (count(array_filter($mutuallyExclusive)) > 1) {
throw self::multipleMutuallyExclusiveArguments();
}
}

public static function multipleMutuallyExclusiveArguments(): DefinitionException
{
return new DefinitionException('The arguments `resolve`, `query` and `find` are mutually exclusive in the `@can` directive.');
$this->validateMutuallyExclusiveArguments(['resolve', 'query', 'find']);
}
}
9 changes: 6 additions & 3 deletions src/GlobalId/NodeDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Nuwave\Lighthouse\GlobalId;

use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\Parser;
Expand Down Expand Up @@ -41,15 +42,15 @@ public static function definition(): string
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
Mutually exclusive with the `model` argument.
Mutually exclusive with `model`.
"""
resolver: String
"""
Specify the class name of the model to use.
This is only needed when the default model detection does not work.
Mutually exclusive with the `model` argument.
Mutually exclusive with `resolver`.
"""
model: String
) on OBJECT
Expand Down Expand Up @@ -84,14 +85,16 @@ public function handleNode(TypeValue $value, \Closure $next): Type
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
$this->validateMutuallyExclusiveArguments(['model', 'resolver']);

if (! $typeDefinition instanceof ObjectTypeDefinitionNode) {
throw new DefinitionException(
"The {$this->name()} directive must only be used on object type definitions, not on {$typeDefinition->kind} {$typeDefinition->name->value}."
);
}

/** @var \GraphQL\Language\AST\NamedTypeNode $namedTypeNode */
$namedTypeNode = Parser::parseType(GlobalIdServiceProvider::NODE, ['noLocation' => true]);
assert($namedTypeNode instanceof NamedTypeNode);
$typeDefinition->interfaces[] = $namedTypeNode;

$globalIdFieldName = config('lighthouse.global_id_field');
Expand Down
16 changes: 9 additions & 7 deletions src/OrderBy/OrderByDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public static function definition(): string
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
Only used when the directive is added on an argument.
"""
columns: [String!]
"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
Only used when the directive is added on an argument.
"""
columnsEnum: String
Expand Down Expand Up @@ -86,21 +86,21 @@ enum OrderByDirection {
"""
input OrderByRelation {
"""
TODO: description
Name of the relation.
"""
relation: String!
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
Mutually exclusive with the `columnsEnum` argument.
Mutually exclusive with `columnsEnum`.
"""
columns: [String!]
"""
Use an existing enumeration type to restrict the allowed columns to a predefined list.
This allowes you to re-use the same enum for multiple fields.
Mutually exclusive with the `columns` argument.
This allows you to re-use the same enum for multiple fields.
Mutually exclusive with `columns`.
"""
columnsEnum: String
}
Expand Down Expand Up @@ -153,6 +153,8 @@ public function manipulateArgDefinition(
FieldDefinitionNode &$parentField,
ObjectTypeDefinitionNode &$parentType
): void {
$this->validateMutuallyExclusiveArguments(['columns', 'columnsEnum']);

if (! $this->hasAllowedColumns() && ! $this->directiveHasArgument('relations')) {
$argDefinition->type = Parser::typeReference('[' . OrderByServiceProvider::DEFAULT_ORDER_BY_CLAUSE . '!]');

Expand Down
15 changes: 12 additions & 3 deletions src/Schema/Directives/AggregateDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

namespace Nuwave\Lighthouse\Schema\Directives;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Execution\BatchLoader\BatchLoaderRegistry;
use Nuwave\Lighthouse\Execution\BatchLoader\RelationBatchLoader;
use Nuwave\Lighthouse\Execution\ModelsLoader\AggregateModelsLoader;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class AggregateDirective extends BaseDirective implements FieldResolver
class AggregateDirective extends BaseDirective implements FieldResolver, FieldManipulator
{
use RelationDirectiveHelpers;

Expand All @@ -38,13 +42,13 @@ function: AggregateFunction!
"""
The relationship with the column to aggregate.
Mutually exclusive with the `model` argument.
Mutually exclusive with `model`.
"""
relation: String
"""
The model with the column to aggregate.
Mutually exclusive with the `relation` argument.
Mutually exclusive with `relation`.
"""
model: String
Expand Down Expand Up @@ -141,4 +145,9 @@ protected function column(): string
{
return $this->directiveArgValue('column');
}

public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType)
{
$this->validateMutuallyExclusiveArguments(['model', 'relation']);
}
}
16 changes: 16 additions & 0 deletions src/Schema/Directives/BaseDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,20 @@ static function (string $classCandidate): bool {

return $modelClass;
}

/**
* Validate at most one of the given mutually exclusive arguments is used.
*
* @param array<string> $names
*/
protected function validateMutuallyExclusiveArguments(array $names): void
{
$given = array_filter($names, [$this, 'directiveHasArgument']);

if (count($given) > 1) {
$namesString = implode(', ', $names);
$givenString = implode(', ', $given);
throw new DefinitionException("The arguments [{$namesString}] for @{$this->name()} are mutually exclusive, found [{$givenString}] on {$this->nodeName()}.");
}
}
}
Loading

0 comments on commit 3f9cc91

Please sign in to comment.