Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run middleware on a per-field basis #395

Merged
merged 11 commits into from
Oct 23, 2018
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the context regenerated here and not passed through from line 75?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might not be necessary anymore. It might be there because middleware is often used for authentication, so we have to make sure the Context is updated with the current user.

If the regeneration is not necessary, i would like to see that covered with a test, then we can remove it. Open to PRs.

$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