Skip to content

Commit

Permalink
Merge pull request #395 from mll-lab/middleware-in-field-resolution
Browse files Browse the repository at this point in the history
Run middleware on a per-field basis
  • Loading branch information
chrissm79 authored Oct 23, 2018
2 parents d57952a + 61bb0cb commit f7840bc
Show file tree
Hide file tree
Showing 21 changed files with 506 additions and 415 deletions.
8 changes: 7 additions & 1 deletion config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@
| Additional configuration for the route group.
| Check options here https://lumen.laravel.com/docs/routing#route-groups
|
| Beware that middleware defined here runs before the GraphQL execution phase.
| This means that errors will cause the whole query to abort and return a
| response that is not spec-compliant. It is preferable to use directives
| to add middleware to single fields in the schema.
| Read more about this in the docs https://lighthouse-php.netlify.com/docs/auth.html#apply-auth-middleware
|
*/
'route' => [
'prefix' => '',
// 'middleware' => ['web','api'], // [ 'loghttp']
// 'middleware' => ['loghttp']
],

/*
Expand Down
2 changes: 1 addition & 1 deletion src/Execution/ContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class ContextFactory implements CreatesContext
*/
public function generate(Request $request): GraphQLContext
{
return new Context($request, $request->user());
return new Context($request);
}
}
11 changes: 0 additions & 11 deletions src/GraphQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\DirectiveRegistry;
use Nuwave\Lighthouse\Schema\MiddlewareRegistry;
use GraphQL\Validator\Rules\DisableIntrospection;
use Nuwave\Lighthouse\Execution\DataLoader\BatchLoader;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
Expand Down Expand Up @@ -326,16 +325,6 @@ public function schema(): TypeRegistry
return $this->types();
}

/**
* @return MiddlewareRegistry
*
* @deprecated Use resolve() instead, will be removed in v3
*/
public function middleware(): MiddlewareRegistry
{
return resolve(MiddlewareRegistry::class);
}

/**
* @return NodeRegistry
*
Expand Down
2 changes: 0 additions & 2 deletions src/Providers/LighthouseServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Execution\ContextFactory;
use Nuwave\Lighthouse\Schema\DirectiveRegistry;
use Nuwave\Lighthouse\Schema\MiddlewareRegistry;
use Nuwave\Lighthouse\Execution\GraphQLValidator;
use Nuwave\Lighthouse\Schema\Source\SchemaStitcher;
use Nuwave\Lighthouse\Schema\Factories\ValueFactory;
Expand Down Expand Up @@ -67,7 +66,6 @@ public function register()
$this->app->singleton(DirectiveRegistry::class);
$this->app->singleton(ExtensionRegistry::class);
$this->app->singleton(NodeRegistry::class);
$this->app->singleton(MiddlewareRegistry::class);
$this->app->singleton(TypeRegistry::class);
$this->app->singleton(CreatesContext::class, ContextFactory::class);

Expand Down
21 changes: 10 additions & 11 deletions src/Schema/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,42 @@
namespace Nuwave\Lighthouse\Schema;

use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Authenticatable as User;
use Illuminate\Contracts\Auth\Authenticatable;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class Context implements GraphQLContext
{
/**
* Http request.
* An instance of the incoming HTTP request.
*
* @var Request
*/
public $request;

/**
* Authenticated user.
* An instance of the currently authenticated user.
*
* May be null since some fields may be accessible without authentication.
*
* @var User|null
* @var Authenticatable|null
*/
public $user;

/**
* Create new context.
*
* @param Request $request
* @param User|null $user
*/
public function __construct(Request $request, User $user = null)
public function __construct(Request $request)
{
$this->request = $request;
$this->user = $user;
$this->user = $request->user();
}

/**
* Get instance of authorized user.
* Get instance of authenticated user.
*
* May be null since some fields may be accessible without authentication.
*
* @return User|null
* @return Authenticatable|null
*/
public function user()
{
Expand Down
170 changes: 134 additions & 36 deletions src/Schema/Directives/Fields/MiddlewareDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,55 @@

namespace Nuwave\Lighthouse\Schema\Directives\Fields;

use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Support\Pipeline;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use GraphQL\Language\AST\FieldDefinitionNode;
use Illuminate\Routing\MiddlewareNameResolver;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Schema\MiddlewareRegistry;
use Nuwave\Lighthouse\Exceptions\ParseException;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\NodeManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;

class MiddlewareDirective extends BaseDirective implements FieldMiddleware
class MiddlewareDirective extends BaseDirective implements FieldMiddleware, NodeManipulator
{
/** @var string todo remove as soon as name() is static itself */
const NAME = 'middleware';

/** @var Pipeline */
protected $pipeline;
/** @var CreatesContext */
protected $createsContext;

/**
* @param Pipeline $pipeline
* @param CreatesContext $createsContext
*/
public function __construct(Pipeline $pipeline, CreatesContext $createsContext)
{
$this->pipeline = $pipeline;
$this->createsContext = $createsContext;
}

/**
* Name of the directive.
*
* @return string
*/
public function name()
public function name(): string
{
return 'middleware';
}
Expand All @@ -27,52 +63,114 @@ public function name()
*
* @return FieldValue
*/
public function handleField(FieldValue $value, \Closure $next)
public function handleField(FieldValue $value, \Closure $next): FieldValue
{
$checks = $this->getChecks($value);

if ($checks) {
$middlewareRegistry = resolve(MiddlewareRegistry::class);

if ('Query' === $value->getNodeName()) {
$middlewareRegistry->registerQuery(
$value->getFieldName(),
$checks
);
} elseif ('Mutation' === $value->getNodeName()) {
$middlewareRegistry->registerMutation(
$value->getFieldName(),
$checks
);
}
}
$middleware = $this->getQualifiedMiddlewareNames(
$this->directiveArgValue('checks')
);
$resolver = $value->getResolver();

return $next($value);
return $next(
$value->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $middleware) {
return $this->pipeline
->send($context->request())
->through($middleware)
->then(function (Request $request) use ($resolver, $root, $args, $resolveInfo){
return $resolver(
$root,
$args,
$this->createsContext->generate($request),
$resolveInfo
);
});
}
)
);
}

/**
* Get middleware checks.
* @param Node $node
* @param DocumentAST $documentAST
*
* @param FieldValue $value
* @throws ParseException
* @throws DirectiveException
*
* @return array|null
* @return DocumentAST
*/
protected function getChecks(FieldValue $value)
public function manipulateSchema(Node $node, DocumentAST $documentAST): DocumentAST
{
if (! in_array($value->getNodeName(), ['Mutation', 'Query'])) {
return null;
return $documentAST->setDefinition(
self::addMiddlewareDirectiveToFields(
$node,
$this->directiveArgValue('checks')
)
);
}

/**
* @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode $objectType
* @param array $middlewareArgValue
*
* @throws ParseException
* @throws DirectiveException
*
* @return ObjectTypeDefinitionNode|ObjectTypeExtensionNode
*/
public static function addMiddlewareDirectiveToFields($objectType, $middlewareArgValue)
{
if ( ! $objectType instanceof ObjectTypeDefinitionNode
&& ! $objectType instanceof ObjectTypeExtensionNode
) {
throw new DirectiveException(
'The ' . self::NAME . ' directive may only be placed on fields or object types.'
);
}

$checks = $this->directiveArgValue('checks');
$middlewareArgValue = collect($middlewareArgValue)
->map(function(string $middleware){
// Add slashes, as re-parsing of the values removes a level of slashes
return addslashes($middleware);
})
->implode('", "');

if (! $checks) {
return null;
}
$middlewareDirective = PartialParser::directive("@middleware(checks: [\"$middlewareArgValue\"])");

if (is_string($checks)) {
$checks = [$checks];
}
$objectType->fields = new NodeList(
collect($objectType->fields)
->map(function (FieldDefinitionNode $fieldDefinition) use ($middlewareDirective) {
// If the field already has middleware defined, skip over it
// Field middleware are more specific then those defined on a type
if (ASTHelper::directiveDefinition($fieldDefinition, MiddlewareDirective::NAME)){
return $fieldDefinition;
}

$fieldDefinition->directives = $fieldDefinition->directives->merge([$middlewareDirective]);

return $fieldDefinition;
})
->toArray()
);

return $objectType;
}

/**
* @param $middlewareArgValue
*
* @return \Illuminate\Support\Collection
*/
protected static function getQualifiedMiddlewareNames($middlewareArgValue): Collection
{
/** @var Router $router */
$router = resolve('router');
$middleware = $router->getMiddleware();
$middlewareGroups = $router->getMiddlewareGroups();

return $checks;
return collect($middlewareArgValue)
->map(function (string $name) use ($middleware, $middlewareGroups) {
return (array) MiddlewareNameResolver::resolve($name, $middleware, $middlewareGroups);
})
->flatten();
}
}
7 changes: 5 additions & 2 deletions src/Schema/Directives/Fields/NamespaceDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
*/
class NamespaceDirective implements Directive
{
/** @var string todo remove as soon as name() is static itself */
const NAME = 'namespace';

/**
* Name of the directive.
*
* @return string
*/
public function name()
public function name(): string
{
return 'namespace';
return self::NAME;
}
}
Loading

0 comments on commit f7840bc

Please sign in to comment.