Skip to content

Commit 79f04c0

Browse files
Automate filter input type creation (#2861)
Developers currently need to manually specify input types in our GraphQL schema file, requiring field names and column mappings to be duplicated. While it's merely a nuisance at the moment, this approach will be completely inadequate once we introduce the ability to filter by relationship existence. This PR cleans up the developer experience by automatically generating filter input types and guessing the input type needed for a filter directive in most cases. That means all developers need to do is add `@filterable` and `@filter` directives to fields and everything just works. Pretty neat! An additional benefit of autogenerating everything is that validation can be added automatically as well. That means that users will now see a validation error when trying to create an invalid filter like this: ```graphql filters: { eq: { id: "123", name: "foo", }, } ``` This validation improvement resolves the issues remaining after #2860.
1 parent ea26769 commit 79f04c0

File tree

4 files changed

+203
-184
lines changed

4 files changed

+203
-184
lines changed

app/GraphQL/Directives/FilterDirective.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Illuminate\Database\Eloquent\Relations\Relation;
1717
use Illuminate\Database\Query\Builder;
1818
use Illuminate\Database\Query\Builder as QueryBuilder;
19+
use Illuminate\Support\Str;
1920
use InvalidArgumentException;
2021
use JsonException;
2122
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
@@ -31,7 +32,7 @@ public static function definition(): string
3132
return /* @lang GraphQL */ <<<'GRAPHQL'
3233
directive @filter(
3334
"Input type to filter on. This should be of the form: <...>FilterInput"
34-
inputType: String!
35+
inputType: String
3536
) on ARGUMENT_DEFINITION
3637
GRAPHQL;
3738
}
@@ -49,7 +50,9 @@ public function manipulateArgDefinition(
4950
$multiFilterName = ASTHelper::qualifiedArgType($argDefinition, $parentField, $parentType) . 'MultiFilterInput';
5051
$argDefinition->type = Parser::namedType($multiFilterName);
5152

52-
$documentAST->setTypeDefinition($this->createMultiFilterInput($multiFilterName, $this->directiveArgValue('inputType')));
53+
$defaultFilterType = Str::replaceEnd('Connection', '', $parentField->type->type->name->value) . 'FilterInput';
54+
$inputType = $this->directiveArgValue('inputType', $defaultFilterType);
55+
$documentAST->setTypeDefinition($this->createMultiFilterInput($multiFilterName, $inputType));
5356
}
5457

5558
/**
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\GraphQL\Directives;
6+
7+
use GraphQL\Error\SyntaxError;
8+
use GraphQL\Language\AST\FieldDefinitionNode;
9+
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
10+
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
11+
use GraphQL\Language\Parser;
12+
use JsonException;
13+
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
14+
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
15+
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
16+
17+
final class FilterableDirective extends BaseDirective implements FieldManipulator
18+
{
19+
public static function definition(): string
20+
{
21+
return /* @lang GraphQL */ <<<'GRAPHQL'
22+
directive @filterable on FIELD_DEFINITION
23+
GRAPHQL;
24+
}
25+
26+
/**
27+
* @throws JsonException
28+
* @throws SyntaxError
29+
*/
30+
public function manipulateFieldDefinition(
31+
DocumentAST &$documentAST,
32+
FieldDefinitionNode &$fieldDefinition,
33+
ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode &$parentType,
34+
): void {
35+
$typeName = $parentType->name->value . 'FilterInput';
36+
37+
// We only have to do this once per type, even though this is called once per filterable directive
38+
if (!array_key_exists($typeName, $documentAST->types)) {
39+
$inputTypeString = "input {$typeName} {" . PHP_EOL;
40+
41+
$allFieldNames = [];
42+
foreach ($parentType->fields as $field) {
43+
$allFieldNames[] = $field->name->value;
44+
}
45+
46+
$filterableFields = [];
47+
foreach ($parentType->fields as $field) {
48+
foreach ($field->directives as $directive) {
49+
if ($directive->name->value === 'filterable') {
50+
$filterableFields[] = $field;
51+
break;
52+
}
53+
}
54+
}
55+
56+
foreach ($filterableFields as $field) {
57+
$name = $field->name->value;
58+
$type = $field->type->name->value ?? $field->type->type->name->value;
59+
$description = $field->description?->value;
60+
61+
// Only allow one field at a time.
62+
$fieldsToExclude = array_filter($allFieldNames, fn ($value): bool => $value !== $name);
63+
$validationDirective = '@rules(apply: ["prohibits:' . implode(',', $fieldsToExclude) . '"])';
64+
65+
// The @rename directive is commonly used, so we handle it explicitly. In the future, we may
66+
// want a more generalized approach which applies any directives which are also valid input directives.
67+
$renameDirective = '';
68+
foreach ($field->directives as $directive) {
69+
if ($directive->name->value === 'rename') {
70+
$renameDirective = '@rename(attribute: "' . $directive->arguments[0]->value->value . '")';
71+
break;
72+
}
73+
}
74+
75+
if ($description !== null) {
76+
$inputTypeString .= '"""' . PHP_EOL . $description . PHP_EOL . '"""' . PHP_EOL;
77+
}
78+
$inputTypeString .= "{$name}: $type $validationDirective $renameDirective" . PHP_EOL;
79+
}
80+
81+
$inputTypeString .= '}';
82+
83+
$documentAST->setTypeDefinition(Parser::inputObjectTypeDefinition($inputTypeString));
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)