From d88376a289b13b5d3d320588d2c8ff68ef1c919e Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sat, 3 Jan 2026 03:52:16 -0300 Subject: [PATCH] feat(graphql): add strict validation to filters (#1041) --- app/GraphQL/Filter/BooleanFilter.php | 12 ++++++++ app/GraphQL/Filter/DateTimeTzFilter.php | 14 ++++++++++ app/GraphQL/Filter/EnumFilter.php | 25 +++++++++++++---- app/GraphQL/Filter/Filter.php | 37 ++++++++++++++++++++++++- app/GraphQL/Filter/FloatFilter.php | 11 ++++++++ app/GraphQL/Filter/IntFilter.php | 11 ++++++++ app/GraphQL/Filter/StringFilter.php | 11 ++++++++ 7 files changed, 115 insertions(+), 6 deletions(-) diff --git a/app/GraphQL/Filter/BooleanFilter.php b/app/GraphQL/Filter/BooleanFilter.php index a3a47343d..ad2ca3166 100644 --- a/app/GraphQL/Filter/BooleanFilter.php +++ b/app/GraphQL/Filter/BooleanFilter.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\GraphQL\Filter; +use App\Rules\Api\IsValidBoolean; use GraphQL\Type\Definition\Type; use Illuminate\Support\Arr; @@ -24,4 +25,15 @@ class BooleanFilter extends Filter fn (string $filterValue): ?bool => filter_var($filterValue, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ); } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return [ + 'required', + new IsValidBoolean(), + ]; + } } diff --git a/app/GraphQL/Filter/DateTimeTzFilter.php b/app/GraphQL/Filter/DateTimeTzFilter.php index a816768f4..4690c3472 100644 --- a/app/GraphQL/Filter/DateTimeTzFilter.php +++ b/app/GraphQL/Filter/DateTimeTzFilter.php @@ -35,4 +35,18 @@ class DateTimeTzFilter extends Filter } ); } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + $allowedDateFormats = array_column(AllowedDateFormat::cases(), 'value'); + $dateFormats = implode(',', $allowedDateFormats); + + return [ + 'required', + "date_format:$dateFormats", + ]; + } } diff --git a/app/GraphQL/Filter/EnumFilter.php b/app/GraphQL/Filter/EnumFilter.php index bd36259fb..402581f3b 100644 --- a/app/GraphQL/Filter/EnumFilter.php +++ b/app/GraphQL/Filter/EnumFilter.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace App\GraphQL\Filter; -use App\Exceptions\GraphQL\ClientValidationException; use BackedEnum; +use Closure; use GraphQL\Type\Definition\Type; use Illuminate\Support\Arr; use Rebing\GraphQL\Support\Facades\GraphQL; @@ -42,10 +42,6 @@ class EnumFilter extends Filter $this->enumClass::cases(), fn (UnitEnum $enum): bool => $enum->name === $filterValue ); - - if (! $enum instanceof BackedEnum) { - throw new ClientValidationException("'{$filterValue}' does not exist in the ".class_basename($this->enumClass).' enum.'); - } } $values[] = $enum ?? $filterValue; @@ -53,4 +49,23 @@ class EnumFilter extends Filter return $values; } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return [ + 'required', + function ($attribute, mixed $value, Closure $fail): void { + if ( + ! is_string($value) + || ! enum_exists($this->enumClass) + || Arr::first($this->enumClass::cases(), fn (UnitEnum $enum): bool => $enum->name === $value) === null + ) { + $fail("'{$value}' does not exist in the ".class_basename($this->enumClass).' enum.'); + } + }, + ]; + } } diff --git a/app/GraphQL/Filter/Filter.php b/app/GraphQL/Filter/Filter.php index 09d343c32..8d9c14bd8 100644 --- a/app/GraphQL/Filter/Filter.php +++ b/app/GraphQL/Filter/Filter.php @@ -5,9 +5,12 @@ declare(strict_types=1); namespace App\GraphQL\Filter; use App\Enums\Http\Api\Filter\ComparisonOperator; +use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Argument\Argument; use App\GraphQL\Argument\FilterArgument; use GraphQL\Type\Definition\Type; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; abstract class Filter { @@ -21,9 +24,14 @@ abstract class Filter protected readonly ?string $column = null, ) {} + public function getFieldName(): string + { + return $this->fieldName; + } + public function getColumn(): string { - return $this->column ?? $this->fieldName; + return $this->column ?? $this->getFieldName(); } /** @@ -39,6 +47,8 @@ abstract class Filter */ public function getFilterValues(array $attemptedFilterValues): array { + $this->validateFilterValues($attemptedFilterValues); + return $this->convertFilterValues($attemptedFilterValues); } @@ -47,6 +57,31 @@ abstract class Filter */ abstract protected function convertFilterValues(array $filterValues): array; + /** + * Validate the filter values against its rules based on types. + */ + protected function validateFilterValues(array $filterValues): void + { + foreach ($filterValues as $filterValue) { + try { + Validator::make( + [$this->fieldName => $filterValue], + [$this->fieldName => $this->getRules()], + )->validate(); + } catch (ValidationException $e) { + throw new ClientValidationException($e->getMessage()); + } + } + } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return []; + } + /** * Allow the Equal operator to the filter. */ diff --git a/app/GraphQL/Filter/FloatFilter.php b/app/GraphQL/Filter/FloatFilter.php index 00f4e3b5b..e5243c487 100644 --- a/app/GraphQL/Filter/FloatFilter.php +++ b/app/GraphQL/Filter/FloatFilter.php @@ -24,4 +24,15 @@ class FloatFilter extends Filter fn (string $filterValue): ?float => filter_var($filterValue, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) ); } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return [ + 'required', + 'numeric', + ]; + } } diff --git a/app/GraphQL/Filter/IntFilter.php b/app/GraphQL/Filter/IntFilter.php index d49e84fb5..df2456ab6 100644 --- a/app/GraphQL/Filter/IntFilter.php +++ b/app/GraphQL/Filter/IntFilter.php @@ -24,4 +24,15 @@ class IntFilter extends Filter fn (string $filterValue): ?int => filter_var($filterValue, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ); } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return [ + 'required', + 'integer', + ]; + } } diff --git a/app/GraphQL/Filter/StringFilter.php b/app/GraphQL/Filter/StringFilter.php index 9985de75f..b7f97ffd8 100644 --- a/app/GraphQL/Filter/StringFilter.php +++ b/app/GraphQL/Filter/StringFilter.php @@ -20,4 +20,15 @@ class StringFilter extends Filter { return $filterValues; } + + /** + * Get the validation rules for the filter. + */ + protected function getRules(): array + { + return [ + 'required', + 'string', + ]; + } }