This commit is contained in:
Kilian Hofmann
2021-06-01 19:55:55 +02:00
parent 052cbe3038
commit d8c489c714
328 changed files with 36645 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
<?php
namespace Nuwave\Lighthouse\Defer;
use Closure;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\GraphQL;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Symfony\Component\HttpFoundation\Response;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse;
class Defer implements CreatesResponse
{
/**
* @var \Nuwave\Lighthouse\Support\Contracts\CanStreamResponse
*/
protected $stream;
/**
* @var \Nuwave\Lighthouse\GraphQL
*/
protected $graphQL;
/**
* @var mixed[]
*/
protected $result = [];
/**
* @var mixed[]
*/
protected $deferred = [];
/**
* @var mixed[]
*/
protected $resolved = [];
/**
* @var bool
*/
protected $acceptFurtherDeferring = true;
/**
* @var bool
*/
protected $isStreaming = false;
/**
* @var int
*/
protected $maxExecutionTime = 0;
/**
* @var int
*/
protected $maxNestedFields = 0;
/**
* @param \Nuwave\Lighthouse\Support\Contracts\CanStreamResponse $stream
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function __construct(CanStreamResponse $stream, GraphQL $graphQL)
{
$this->stream = $stream;
$this->graphQL = $graphQL;
$this->maxNestedFields = config('lighthouse.defer.max_nested_fields', 0);
}
/**
* Set the tracing directive on all fields of the query to enable tracing them.
*
* @param \Nuwave\Lighthouse\Events\ManipulateAST $manipulateAST
* @return void
*/
public function handleManipulateAST(ManipulateAST $manipulateAST): void
{
ASTHelper::attachDirectiveToObjectTypeFields(
$manipulateAST->documentAST,
PartialParser::directive('@deferrable')
);
$manipulateAST->documentAST->setDirectiveDefinition(
PartialParser::directiveDefinition('
"""
Use this directive on expensive or slow fields to resolve them asynchronously.
Must not be placed upon:
- Non-Nullable fields
- Mutation root fields
"""
directive @defer(if: Boolean = true) on FIELD
')
);
}
/**
* @return bool
*/
public function isStreaming(): bool
{
return $this->isStreaming;
}
/**
* Register deferred field.
*
* @param \Closure $resolver
* @param string $path
* @return mixed
*/
public function defer(Closure $resolver, string $path)
{
if ($data = Arr::get($this->result, "data.{$path}")) {
return $data;
}
if ($this->isDeferred($path) || ! $this->acceptFurtherDeferring) {
return $this->resolve($resolver, $path);
}
$this->deferred[$path] = $resolver;
}
/**
* @param \Closure $originalResolver
* @param string $path
* @return mixed
*/
public function findOrResolve(Closure $originalResolver, string $path)
{
if (! $this->hasData($path)) {
if (isset($this->deferred[$path])) {
unset($this->deferred[$path]);
}
return $this->resolve($originalResolver, $path);
}
return Arr::get($this->result, "data.{$path}");
}
/**
* Resolve field with data or resolver.
*
* @param \Closure $originalResolver
* @param string $path
* @return mixed
*/
public function resolve(Closure $originalResolver, string $path)
{
$isDeferred = $this->isDeferred($path);
$resolver = $isDeferred
? $this->deferred[$path]
: $originalResolver;
if ($isDeferred) {
$this->resolved[] = $path;
unset($this->deferred[$path]);
}
return $resolver();
}
/**
* @param string $path
* @return bool
*/
public function isDeferred(string $path): bool
{
return isset($this->deferred[$path]);
}
/**
* @param string $path
* @return bool
*/
public function hasData(string $path): bool
{
return Arr::has($this->result, "data.{$path}");
}
/**
* Return either a final response or a stream of responses.
*
* @param mixed[] $result
* @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public function createResponse(array $result): Response
{
if (empty($this->deferred)) {
return response($result);
}
return response()->stream(
function () use ($result): void {
$nested = 1;
$this->result = $result;
$this->isStreaming = true;
$this->stream->stream($result, [], empty($this->deferred));
if ($executionTime = config('lighthouse.defer.max_execution_ms', 0)) {
$this->maxExecutionTime = microtime(true) + ($executionTime * 1000);
}
// TODO: Allow nested_levels to be set in config to break out of loop early.
while (
count($this->deferred)
&& ! $this->executionTimeExpired()
&& ! $this->maxNestedFieldsResolved($nested)
) {
$nested++;
$this->executeDeferred();
}
// We've hit the max execution time or max nested levels of deferred fields.
// We process remaining deferred fields, but are no longer allowing additional
// fields to be deferred.
if (count($this->deferred)) {
$this->acceptFurtherDeferring = false;
$this->executeDeferred();
}
},
200,
[
// TODO: Allow headers to be set in config
'X-Accel-Buffering' => 'no',
'Content-Type' => 'multipart/mixed; boundary="-"',
]
);
}
/**
* @param int $time
* @return void
*/
public function setMaxExecutionTime(int $time): void
{
$this->maxExecutionTime = $time;
}
/**
* Override max nested fields.
*
* @param int $max
* @return void
*/
public function setMaxNestedFields(int $max): void
{
$this->maxNestedFields = $max;
}
/**
* Check if the maximum execution time has expired.
*
* @return bool
*/
protected function executionTimeExpired(): bool
{
if ($this->maxExecutionTime === 0) {
return false;
}
return $this->maxExecutionTime <= microtime(true);
}
/**
* Check if the maximum number of nested field has been resolved.
*
* @param int $nested
* @return bool
*/
protected function maxNestedFieldsResolved(int $nested): bool
{
if ($this->maxNestedFields === 0) {
return false;
}
return $nested >= $this->maxNestedFields;
}
/**
* Execute deferred fields.
*
* @return void
*/
protected function executeDeferred(): void
{
$this->result = app()->call(
[$this->graphQL, 'executeRequest']
);
$this->stream->stream(
$this->result,
$this->resolved,
empty($this->deferred)
);
$this->resolved = [];
}
}
@@ -0,0 +1,44 @@
<?php
namespace Nuwave\Lighthouse\Defer;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
class DeferServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @param \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory $directiveFactory
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function boot(DirectiveFactory $directiveFactory, Dispatcher $dispatcher): void
{
$directiveFactory->addResolved(
DeferrableDirective::NAME,
DeferrableDirective::class
);
$dispatcher->listen(
ManipulateAST::class,
Defer::class.'@handleManipulateAST'
);
}
/**
* Register any application services.
*
* @return void
*/
public function register(): void
{
$this->app->singleton(Defer::class);
$this->app->singleton(CreatesResponse::class, Defer::class);
}
}
@@ -0,0 +1,138 @@
<?php
namespace Nuwave\Lighthouse\Defer;
use Closure;
use GraphQL\Error\Error;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Language\AST\NonNullTypeNode;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class DeferrableDirective extends BaseDirective implements DefinedDirective, FieldMiddleware
{
const NAME = 'deferrable';
const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD = 'The @defer directive cannot be used on a root mutation field.';
const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD = 'The @defer directive cannot be used on a Non-Nullable field.';
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Do not use this directive directly, it is automatically added to the schema
when using the defer extension.
"""
directive @deferrable on FIELD_DEFINITION
SDL;
}
/**
* @var \Nuwave\Lighthouse\Defer\Defer
*/
protected $defer;
/**
* @param \Nuwave\Lighthouse\Defer\Defer $defer
* @return void
*/
public function __construct(Defer $defer)
{
$this->defer = $defer;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return self::NAME;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$previousResolver = $fieldValue->getResolver();
$fieldType = $fieldValue->getField()->type;
$fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver, $fieldType) {
$wrappedResolver = function () use ($previousResolver, $root, $args, $context, $resolveInfo) {
return $previousResolver($root, $args, $context, $resolveInfo);
};
$path = implode('.', $resolveInfo->path);
if ($this->shouldDefer($fieldType, $resolveInfo)) {
return $this->defer->defer($wrappedResolver, $path);
}
return $this->defer->isStreaming()
? $this->defer->findOrResolve($wrappedResolver, $path)
: $previousResolver($root, $args, $context, $resolveInfo);
}
);
return $next($fieldValue);
}
/**
* Determine if field should be deferred.
*
* @param \GraphQL\Language\AST\TypeNode $fieldType
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return bool
*
* @throws \GraphQL\Error\Error
*/
protected function shouldDefer(TypeNode $fieldType, ResolveInfo $resolveInfo): bool
{
foreach ($resolveInfo->fieldNodes as $fieldNode) {
$deferDirective = ASTHelper::directiveDefinition($fieldNode, 'defer');
if (! $deferDirective) {
return false;
}
if ($resolveInfo->parentType->name === 'Mutation') {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD);
}
if (! ASTHelper::directiveArgValue($deferDirective, 'if', true)) {
return false;
}
$skipDirective = ASTHelper::directiveDefinition($fieldNode, 'skip');
if (
$skipDirective
&& ASTHelper::directiveArgValue($skipDirective, 'if') === true
) {
return false;
}
$includeDirective = ASTHelper::directiveDefinition($fieldNode, 'include');
if (
$includeDirective
&& ASTHelper::directiveArgValue($includeDirective, 'if') === false
) {
return false;
}
}
if ($fieldType instanceof NonNullTypeNode) {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD);
}
return true;
}
}