Graphql-laravel: Resolver Middleware

Created on 25 Feb 2020  路  8Comments  路  Source: rebing/graphql-laravel

GraphQL-aware middleware solve a variety of problems for me, a while ago I needed to be able to Logstash various stats about the queries going through my API (e.g. what args were passed, what fields were requested), and more recently I wanted to move authorization ahead of validation after I learnt about https://github.com/rebing/graphql-laravel/issues/481.

Turns out pipelines can do a lot of the heavy lifting here, this is what I'm running with currently, it'd be interesting to wrap this up into native support.

Middleware resolver:

<?php

namespace App\GraphQL\Traits;

use Closure;
use App\GraphQL\Middleware;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Facades\App;

trait HasMiddleware
{
    protected $middleware = [
        Middleware\Authenticate::class,
        Middleware\Logstash::class,
        Middleware\Paginator::class,
    ];

    protected function getResolver() : ?Closure
    {
        $resolver = parent::getResolver();

        if (!$resolver) {
            return null;
        }

        return function ($root, ...$arguments) use ($resolver) {
            return app(Pipeline::class)
                ->send(array_merge([$this], $arguments))
                ->through($this->middleware)
                ->via('resolve')
                ->then(function ($arguments) use ($resolver, $root) {
                    $result = $resolver($root, ...array_slice($arguments, 1));

                    App::terminating(function () use ($arguments, $result) {
                        foreach ($this->middleware as $name) {
                            $instance = App::make($name);

                            if (method_exists($instance, 'terminate')) {
                                $instance->terminate($this, ...array_slice($arguments, 1), ...[$result]);
                            }
                        }
                    });

                    return $result;
                });
        };
    }
}

Middleware base class:

<?php

namespace App\GraphQL\Middleware;

use Closure;

abstract class Middleware
{
    public function resolve(array $arguments, Closure $next)
    {
        return $this->handle(...$arguments, ...[
            function (...$arguments) use ($next) {
                return $next($arguments);
            }
        ]);
    }
}

Example middleware:

<?php

namespace App\GraphQL\Middleware;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate;

class Paginator extends Middleware
{
    public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
    {
        Illuminate\Pagination\Paginator::currentPageResolver(function () use ($args) {
            return $args['pagination']['page'] ?? 1;
        });

        return $next($root, $args, $context, $info);
    }

    // public function terminate($root, $args, $context, ResolveInfo $info, $result) {
    //     // optional
    // }
}
enhancement

Most helpful comment

@crissi have updated my example to include support for terminate 馃

All 8 comments

I'm open / interested in this!

Would be interesting what other contributors think /cc @crissi

@mfn alright great, I've updated my example above, I worked out how to get rid of the awkward array $arguments problem via a base middleware class and wrapping the $next closure.

Also I refined the $root usage a bit, from within middleware $root is the query/mutation instance, and then the original root (probably null?) is restored in the then callback... is this an acceptable usage of $root?

Access to the query/mutation instance via $root is useful for defining your own DSLs.

Here's an example of a middleware adding support for a query-level authenticate method that is mostly the same as authorize, but gets called before validation:

<?php

namespace App\GraphQL\Middleware;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Facades\Auth;
use Rebing\GraphQL\Error\AuthorizationError;

/**
 * Authenticate is variation on authorize, but it happens before validation
 *
 * https://github.com/rebing/graphql-laravel/issues/481
 */
class Authenticate extends Middleware
{
    public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
    {
        if (!$this->authenticate($root, $args, $context, $info)) {
            throw new AuthorizationError('Unauthorized');
        }

        return $next($root, $args, $context, $info);
    }

    private function authenticate($root, $args, $context, ResolveInfo $info) : bool
    {
        if (method_exists($root, 'authenticate')) {
            return $root->authenticate($root, $args, $context, $info);
        }

        return Auth::check();
    }
}

@mfn any further thoughts about the above? I'll wrap this up into a PR if we're satisfied with the implementation. The only question I have is whether it should be on Field or a trait, the former is fine I guess? The $middleware array would be empty by default.

I like it, I am also using a middleware for logging mutations, I am using a terminable middleware, so could be cool to make it easy to use that as well https://laravel.com/docs/5.0/middleware#terminable-middleware

@crissi have updated my example to include support for terminate 馃

Great Idea. Is it work in current version?

PR is finally merged, apologies for this super long delay. 2020 aint easy on anyone.

For anyone interested => you can test this via https://github.com/rebing/graphql-laravel/releases/tag/6.1.0-rc1 , will try to wait for some feedback before making a non-pre release.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

edgarsn picture edgarsn  路  4Comments

idubz33 picture idubz33  路  5Comments

niiapa picture niiapa  路  8Comments

jeekymojica picture jeekymojica  路  7Comments

shengslogar picture shengslogar  路  6Comments