Skip to content

Instantly share code, notes, and snippets.

@tasinttttttt
Last active July 26, 2023 08:39
Show Gist options
  • Save tasinttttttt/82e10af25e265bf797e2cb3deca9a36e to your computer and use it in GitHub Desktop.
Save tasinttttttt/82e10af25e265bf797e2cb3deca9a36e to your computer and use it in GitHub Desktop.
Lighthouse 6 validation is processed before guarding Fix
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// Register the custom graphql context
$this->app->bind(
\Nuwave\Lighthouse\Support\Contracts\CreatesContext::class,
\App\GraphQL\Context\ContextFactory::class
);
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
}
}
<?php
// app/GraphQL/Directives/CanDirective.php
declare(strict_types=1);
namespace App\GraphQL\Directives;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Auth\CanDirective as LighthouseCanDirective;
use Nuwave\Lighthouse\Execution\Resolved;
use Nuwave\Lighthouse\Execution\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Utils;
class CanDirective extends LighthouseCanDirective
{
public function handleField(FieldValue $fieldValue): void
{
$ability = $this->directiveArgValue('ability');
$resolved = $this->directiveArgValue('resolved');
$fieldValue->wrapResolver(fn (callable $resolver): \Closure => function (mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $ability, $resolved) {
$gate = $this->gate->forUser($context->user());
$checkArguments = $this->buildCheckArguments($args);
if ($resolved) {
return Resolved::handle(
$resolver($root, $args, $context, $resolveInfo),
function ($modelLike) use ($gate, $ability, $checkArguments) {
$modelOrModels = $modelLike instanceof Paginator
? $modelLike->items()
: $modelLike;
Utils::applyEach(function (?Model $model) use ($gate, $ability, $checkArguments): void {
$this->authorize($gate, $ability, $model, $checkArguments);
}, $modelOrModels);
return $modelLike;
},
);
}
// do the checks
foreach ($this->modelsToCheck($root, $args, $context, $resolveInfo) as $model) {
$this->authorize($gate, $ability, $model, $checkArguments);
}
// then throw validation error if exists in the context
if ($context->validationError()) {
throw $context->validationError();
}
return $resolver($root, $args, $context, $resolveInfo);
});
}
}
<?php
// app/GraphQL/Context/Context.php
namespace App\GraphQL\Context;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Auth\AuthServiceProvider;
use Nuwave\Lighthouse\Exceptions\ValidationException;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
final class Context implements GraphQLContext
{
/** First validation exception */
public ?ValidationException $validationError = null;
/** An instance of the currently authenticated user. */
public ?Authenticatable $user = null;
public function __construct(
/**
* An instance of the incoming HTTP request.
*/
public Request $request,
) {
foreach (AuthServiceProvider::guards() as $guard) {
$this->user = $request->user($guard);
if (isset($this->user)) {
break;
}
}
}
public function request(): ?Request
{
return $this->request;
}
public function user(): ?Authenticatable
{
return $this->user;
}
public function setUser(?Authenticatable $user): void
{
$this->user = $user;
}
public function validationError(): ?ValidationException
{
return $this->validationError;
}
public function setValidationError(?ValidationException $validationError): void
{
$this->validationError = $validationError;
}
}
<?php
// app/GraphQL/Context/ContextFactory.php
declare(strict_types=1);
namespace App\GraphQL\Context;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
final class ContextFactory implements CreatesContext
{
public function generate(?Request $request): GraphQLContext
{
return new Context($request);
}
}
<?php
// app/GraphQL/Directives/GuardDirective.php
declare(strict_types=1);
namespace App\GraphQL\Directives;
use Nuwave\Lighthouse\Auth\AuthServiceProvider;
use Nuwave\Lighthouse\Auth\GuardDirective as LighthouseGuardDirective;
use Nuwave\Lighthouse\Execution\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class GuardDirective extends LighthouseGuardDirective
{
public function handleField(FieldValue $fieldValue): void
{
$fieldValue->wrapResolver(fn (callable $resolver): \Closure => function (mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
$guards = $this->directiveArgValue('with', AuthServiceProvider::guards());
// do the checks
$context->setUser($this->authenticate($guards));
// then throw validation error if exists in the context
if ($context->validationError()) {
throw $context->validationError();
}
return $resolver($root, $args, $context, $resolveInfo);
});
}
}
// config/lighthouse.php
...
'namespaces' => [
'directives' => ['App\\GraphQL\\Directives'], // important!
],
...
'field_middleware' => [
\Nuwave\Lighthouse\Schema\Directives\TrimDirective::class,
\Nuwave\Lighthouse\Schema\Directives\ConvertEmptyStringsToNullDirective::class,
\Nuwave\Lighthouse\Schema\Directives\SanitizeDirective::class,
// \Nuwave\Lighthouse\Validation\ValidateDirective::class, // overriding the default Validation
\App\Middleware\LighthouseValidateDirectiveFix::class, // with this fixed one
\Nuwave\Lighthouse\Schema\Directives\TransformArgsDirective::class,
\Nuwave\Lighthouse\Schema\Directives\SpreadDirective::class,
\Nuwave\Lighthouse\Schema\Directives\RenameArgsDirective::class,
\Nuwave\Lighthouse\Schema\Directives\DropArgsDirective::class,
],
<?php
// app/Middleware/LighthouseValidateDirectiveFix.php
declare(strict_types=1);
namespace App\Middleware;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Nuwave\Lighthouse\Exceptions\ValidationException;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Validation\RulesGatherer;
use Nuwave\Lighthouse\Validation\ValidateDirective;
class LighthouseValidateDirectiveFix extends ValidateDirective
{
public $error = null;
public function handleField(FieldValue $fieldValue): void
{
$self = $this;
$hasGuardOrCanDirective = $this->fieldHasGuardOrCanDirective($fieldValue);
$fieldValue->addArgumentSetTransformer(static function (ArgumentSet $argumentSet, ResolveInfo $resolveInfo) use ($self, $hasGuardOrCanDirective): ArgumentSet {
$rulesGatherer = new RulesGatherer($argumentSet);
$validationFactory = Container::getInstance()->make(ValidationFactory::class);
$validator = $validationFactory->make(
$argumentSet->toArray(),
$rulesGatherer->rules,
$rulesGatherer->messages,
$rulesGatherer->attributes,
);
if ($validator->fails()) {
$path = implode('.', $resolveInfo->path);
$error = new ValidationException("Validation failed for the field [{$path}].", $validator);
if ($hasGuardOrCanDirective) {
$self->error = $error;
} else {
throw $error;
}
}
return $argumentSet;
});
$fieldValue->wrapResolver(fn (callable $resolver): \Closure => function (mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $self) {
if ($self->error) {
// Saving the error in the custom graphqlcontext
$context->setValidationError($self->error);
}
return $resolver($root, $args, $context, $resolveInfo);
});
}
protected function fieldHasGuardOrCanDirective(FieldValue $fieldValue): bool
{
/** @var DirectiveNode $directive */
foreach ($fieldValue->getField()->directives as $directive) {
if (in_array($directive->name->value, ['guard', 'can'])) {
return true;
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment