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
@@ -0,0 +1,36 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository;
class ClearCacheCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lighthouse:clear-cache';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear the cache for the GraphQL AST.';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function handle(Repository $cache): void
{
$cache->forget(config('lighthouse.cache.key'));
$this->info('GraphQL AST schema cache deleted.');
}
}
@@ -0,0 +1,153 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\Command;
use HaydenPierce\ClassFinder\ClassFinder;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\DirectiveNamespacer;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class IdeHelperCommand extends Command
{
const GENERATED_NOTICE = <<<'SDL'
# File generated by "php artisan lighthouse:ide-helper".
# Do not edit this file directly.
# This file should be ignored by git.
SDL;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lighthouse:ide-helper';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Gather all schema directive definitions and write them to a file.';
/**
* Execute the console command.
*
* @param \Nuwave\Lighthouse\Schema\DirectiveNamespacer $directiveNamespaces
* @return int
*/
public function handle(DirectiveNamespacer $directiveNamespaces): int
{
if (! class_exists('HaydenPierce\ClassFinder\ClassFinder')) {
$this->error(
"This command requires haydenpierce/class-finder. Install it by running:\n"
."\n"
." composer require --dev haydenpierce/class-finder\n"
);
return 1;
}
$directiveClasses = $this->scanForDirectives(
$directiveNamespaces->gather()
);
$schema = $this->buildSchemaString($directiveClasses);
$filePath = static::filePath();
file_put_contents($filePath, $schema);
$this->info("Wrote schema directive definitions to $filePath.");
return 0;
}
/**
* Scan the given namespaces for directive classes.
*
* @param string[] $directiveNamespaces
* @return string[]
*/
protected function scanForDirectives(array $directiveNamespaces): array
{
$directives = [];
foreach ($directiveNamespaces as $directiveNamespace) {
try {
$classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
} catch (ClassFinderException $classFinderException) {
// TODO remove if https://gitlab.com/hpierce1102/ClassFinder/merge_requests/16 is merged
// The ClassFinder throws if no classes are found. Since we can not know
// in advance if the user has defined custom directives, this behaviour is problematic.
continue;
}
foreach ($classesInNamespace as $class) {
$reflection = new \ReflectionClass($class);
if (! $reflection->isInstantiable()) {
continue;
}
if (! is_a($class, Directive::class, true)) {
continue;
}
/** @var \Nuwave\Lighthouse\Support\Contracts\Directive $instance */
$instance = app($class);
$name = $instance->name();
// The directive was already found, so we do not add it twice
if (isset($directives[$name])) {
continue;
}
$directives[$name] = $class;
}
}
return $directives;
}
/**
* @param string[] $directiveClasses
* @return string
*/
protected function buildSchemaString(array $directiveClasses): string
{
$schema = self::GENERATED_NOTICE;
foreach ($directiveClasses as $name => $directiveClass) {
$definition = $this->define($name, $directiveClass);
$schema .= "\n"
."# Directive class: $directiveClass\n"
.$definition."\n";
}
return $schema;
}
protected function define(string $name, string $directiveClass): string
{
if (is_a($directiveClass, DefinedDirective::class, true)) {
/** @var DefinedDirective $directiveClass */
$definition = $directiveClass::definition();
// This operation throws if the schema definition is invalid
PartialParser::directiveDefinition($definition);
return trim($definition);
} else {
return '# Add a proper definition by implementing '.DefinedDirective::class."\n"
."directive @{$name}";
}
}
public static function filePath(): string
{
return base_path().'/schema-directives.graphql';
}
}
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class InterfaceCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:interface';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL interface type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Interface';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.interfaces');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/typeResolver.stub';
}
}
@@ -0,0 +1,22 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\GeneratorCommand;
abstract class LighthouseGeneratorCommand extends GeneratorCommand
{
/**
* Get the desired class name from the input.
*
* As a typical workflow would be to write the schema first and then copy-paste
* a field name to generate a class for it, we uppercase it so the user does
* not run into unnecessary errors. You're welcome.
*
* @return string
*/
protected function getNameInput(): string
{
return ucfirst(trim($this->argument('name')));
}
}
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class MutationCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:mutation';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Mutation type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Mutation';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.mutations');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/field.stub';
}
}
@@ -0,0 +1,54 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Nuwave\Lighthouse\GraphQL;
use Illuminate\Console\Command;
use GraphQL\Utils\SchemaPrinter;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Filesystem\Filesystem;
class PrintSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = '
lighthouse:print-schema
{--W|write : Write the output to a file}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Compile the final GraphQL schema and print the result.';
/**
* Execute the console command.
*
* @param \Illuminate\Cache\Repository $cache
* @param \Illuminate\Contracts\Filesystem\Filesystem $storage
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function handle(Repository $cache, Filesystem $storage, GraphQL $graphQL): void
{
// Clear the cache so this always gets the current schema
$cache->forget(config('lighthouse.cache.key'));
$schema = SchemaPrinter::doPrint(
$graphQL->prepSchema()
);
if ($this->option('write')) {
$storage->put('lighthouse-schema.graphql', $schema);
$this->info('Wrote schema to the default file storage (usually storage/app) as "lighthouse-schema.graphql".');
} else {
$this->info($schema);
}
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class QueryCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:query';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Query type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Query';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.queries');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/field.stub';
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class ScalarCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:scalar';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL scalar type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Scalar';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.scalars');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/scalar.stub';
}
}
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class SubscriptionCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:subscription';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Subscription type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Subscription';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.subscriptions');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/subscription.stub';
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace Nuwave\Lighthouse\Console;
class UnionCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:union';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL union type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Union';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
{
return config('lighthouse.namespaces.unions');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/typeResolver.stub';
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Nuwave\Lighthouse\GraphQL;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository;
class ValidateSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lighthouse:validate-schema';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Validate the GraphQL schema definition.';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function handle(Repository $cache, GraphQL $graphQL): void
{
// Clear the cache so this always validates the current schema
$cache->forget(config('lighthouse.cache.key'));
$graphQL->prepSchema()->assertValid();
$this->info('The defined schema is valid.');
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace DummyNamespace;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class DummyClass
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param mixed[] $args The arguments that were passed into the field.
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context Arbitrary data that is shared between all fields of a single query.
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
* @return mixed
*/
public function __invoke($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
// TODO implement the resolver
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace DummyNamespace;
use GraphQL\Type\Definition\ScalarType;
/**
* Read more about scalars here http://webonyx.github.io/graphql-php/type-system/scalar-types/
*/
class DummyClass extends ScalarType
{
/**
* Serializes an internal value to include in a response.
*
* @param mixed $value
* @return mixed
*/
public function serialize($value)
{
// Assuming the internal representation of the value is always correct
return $value;
// TODO validate if it might be incorrect
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public function parseValue($value)
{
// TODO implement validation
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
*
* E.g.
* {
* user(email: "user@example.com")
* }
*
* @param \GraphQL\Language\AST\Node $valueNode
* @param mixed[]|null $variables
* @return mixed
*/
public function parseLiteral($valueNode, ?array $variables = null)
{
// TODO implement validation
return $valueNode->value;
}
}
@@ -0,0 +1,34 @@
<?php
namespace DummyNamespace;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
class DummyClass extends GraphQLSubscription
{
/**
* Check if subscriber is allowed to listen to the subscription.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorize(Subscriber $subscriber, Request $request): bool
{
// TODO implement authorize
}
/**
* Filter which subscribers should receive the subscription.
*
* @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber
* @param mixed $root
* @return bool
*/
public function filter(Subscriber $subscriber, $root): bool
{
// TODO implement filter
}
}
@@ -0,0 +1,43 @@
<?php
namespace DummyNamespace;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\TypeRegistry;
class DummyClass
{
/**
* The type registry.
*
* @var \Nuwave\Lighthouse\Schema\TypeRegistry
*/
protected $typeRegistry;
/**
* Constructor.
*
* @param \Nuwave\Lighthouse\Schema\TypeRegistry $typeRegistry
* @return void
*/
public function __construct(TypeRegistry $typeRegistry)
{
$this->typeRegistry = $typeRegistry;
}
/**
* Decide which GraphQL type a resolved value has.
*
* @param mixed $rootValue The value that was resolved by the field. Usually an Eloquent model.
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return \GraphQL\Type\Definition\Type
*/
public function __invoke($rootValue, GraphQLContext $context, ResolveInfo $resolveInfo): Type
{
// Default to getting a type with the same name as the passed in root value
// TODO implement your own resolver logic - if the default is fine, just delete this class
return $this->typeRegistry->get(class_basename($rootValue));
}
}
+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;
}
}
@@ -0,0 +1,15 @@
<?php
namespace Nuwave\Lighthouse\Events;
/**
* Fires after a query was resolved.
*
* Listeners of this event may return an instance of
* @see \Nuwave\Lighthouse\Execution\ExtensionsResponse
* that is then added to the response.
*/
class BuildExtensionsResponse
{
//
}
@@ -0,0 +1,32 @@
<?php
namespace Nuwave\Lighthouse\Events;
/**
* Fires before building the AST from the user-defined schema string.
*
* Listeners may return a schema string,
* which is added to the user schema.
*
* Only fires once if schema caching is active.
*/
class BuildSchemaString
{
/**
* The root schema that was defined by the user.
*
* @var string
*/
public $userSchema;
/**
* BuildSchemaString constructor.
*
* @param string $userSchema
* @return void
*/
public function __construct(string $userSchema)
{
$this->userSchema = $userSchema;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Nuwave\Lighthouse\Events;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
/**
* Fires after the AST was built but before the executable schema is built.
*
* Listeners may mutate the $documentAST and make programmatic
* changes to the schema.
*
* Only fires once if schema caching is active.
*/
class ManipulateAST
{
/**
* The AST that can be manipulated.
*
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
public $documentAST;
/**
* BuildSchemaString constructor.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @return void
*/
public function __construct(DocumentAST &$documentAST)
{
$this->documentAST = $documentAST;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Nuwave\Lighthouse\Events;
use GraphQL\Executor\ExecutionResult;
/**
* Fires after resolving each individual query.
*
* This gives listeners an easy way to manipulate the query
* result without worrying about batched execution.
*/
class ManipulateResult
{
/**
* The result of resolving an individual query.
*
* @var \GraphQL\Executor\ExecutionResult
*/
public $result;
/**
* ManipulateResult constructor.
*
* @param \GraphQL\Executor\ExecutionResult $result
* @return void
*/
public function __construct(ExecutionResult &$result)
{
$this->result = $result;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Nuwave\Lighthouse\Events;
/**
* Fires when the directive factory is constructed.
*
* Listeners may return one or more strings that are used as the base
* namespace for locating directives.
*
* @see \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory
*/
class RegisterDirectiveNamespaces
{
//
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Nuwave\Lighthouse\Events;
use Carbon\Carbon;
/**
* Fires right before resolving an individual query.
*
* Might happen multiple times in a single request if
* query batching is used.
*/
class StartExecution
{
/**
* The point in time when the query execution started.
*
* @var \Carbon\Carbon
*/
public $moment;
/**
* StartRequest constructor.
*
* @return void
*/
public function __construct()
{
$this->moment = Carbon::now();
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Nuwave\Lighthouse\Events;
use Carbon\Carbon;
use Nuwave\Lighthouse\Execution\GraphQLRequest;
/**
* Fires right after a request reaches the GraphQLController.
*
* Can be used for logging or for measuring and monitoring
* the time a request takes to resolve.
*
* @see \Nuwave\Lighthouse\Support\Http\Controllers\GraphQLController
*/
class StartRequest
{
/**
* GraphQL request instance.
*
* @var \Nuwave\Lighthouse\Execution\GraphQLRequest
*/
public $request;
/**
* The point in time when the request started.
*
* @var \Carbon\Carbon
*/
public $moment;
/**
* StartRequest constructor.
*
* @param \Nuwave\Lighthouse\Execution\GraphQLRequest $request
* @return void
*/
public function __construct(GraphQLRequest $request)
{
$this->request = $request;
$this->moment = Carbon::now();
}
}
@@ -0,0 +1,43 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Illuminate\Auth\AuthenticationException as IlluminateAuthenticationException;
class AuthenticationException extends IlluminateAuthenticationException implements RendersErrorsExtensions
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'authentication';
}
/**
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
*/
public function extensionsContent(): array
{
return ['guards' => $this->guards];
}
}
@@ -0,0 +1,38 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use GraphQL\Error\ClientAware;
use Illuminate\Auth\Access\AuthorizationException as IlluminateAuthorizationException;
class AuthorizationException extends IlluminateAuthorizationException implements ClientAware
{
/**
* @var string
*/
const CATEGORY = 'authorization';
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return self::CATEGORY;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
class DefinitionException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';
}
}
@@ -0,0 +1,33 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
class DirectiveException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';
}
}
@@ -0,0 +1,41 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use GraphQL\Error\Error;
class GenericException extends Error
{
/**
* The category.
*
* @var string
*/
protected $category = 'generic';
/**
* Set the contents that will be rendered under the "extensions" key of the error response.
*
* @param mixed $extensions
* @return $this
*/
public function setExtensions($extensions): self
{
$this->extensions = (array) $extensions;
return $this;
}
/**
* Set the category that will be rendered under the "extensions" key of the error response.
*
* @param string $category
* @return $this
*/
public function setCategory(string $category): self
{
$this->category = $category;
return $this;
}
}
@@ -0,0 +1,10 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
class InvalidDriverException extends Exception
{
//
}
@@ -0,0 +1,33 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
class ParseException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';
}
}
@@ -0,0 +1,22 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use GraphQL\Error\ClientAware;
/**
* Exceptions may implement this interface.
*
* This enables them to render additional content to the errors
* entry that is returned to the client when it occurs.
*/
interface RendersErrorsExtensions extends ClientAware
{
/**
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
*/
public function extensionsContent(): array;
}
@@ -0,0 +1,35 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use InvalidArgumentException;
use GraphQL\Error\ClientAware;
class SubscriptionException extends InvalidArgumentException implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
*
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
*
* @return string
*/
public function getCategory(): string
{
return 'subscription';
}
}
@@ -0,0 +1,37 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
class ValidationException extends \Illuminate\Validation\ValidationException implements RendersErrorsExtensions
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @return bool
*/
public function isClientSafe()
{
return true;
}
/**
* Returns string describing a category of the error.
*
* @return string
*/
public function getCategory()
{
return 'validation';
}
/**
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
*/
public function extensionsContent(): array
{
return ['validation' => $this->errors()];
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Http\Request;
abstract class BaseRequest implements GraphQLRequest
{
/**
* The current batch index.
*
* Is null if we are not resolving a batched query.
*
* @var int|null
*/
protected $batchIndex;
/**
* Get the contents of a field by key.
*
* This is expected to take batched requests into consideration.
*
* @param string $key
* @return array|string|null
*/
abstract protected function fieldValue(string $key);
/**
* Are there more batched queries to process?
*
* @return bool
*/
abstract protected function hasMoreBatches(): bool;
/**
* Construct this from a HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
abstract public function __construct(Request $request);
/**
* Get the contained GraphQL query string.
*
* @return string
*/
public function query(): string
{
return $this->fieldValue('query');
}
/**
* Get the operationName of the current request.
*
* @return string|null
*/
public function operationName(): ?string
{
return $this->fieldValue('operationName');
}
/**
* Is the current query a batched query?
*
* @return bool
*/
public function isBatched(): bool
{
return ! is_null($this->batchIndex);
}
/**
* Get the index of the current batch.
*
* Returns null if we are not resolving a batched query.
*
* @return int|null
*/
public function batchIndex(): ?int
{
return $this->batchIndex;
}
/**
* Advance the batch index and indicate if there are more batches to process.
*
* @return bool
*/
public function advanceBatchIndex(): bool
{
if ($result = $this->hasMoreBatches()) {
$this->batchIndex++;
}
return $result;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
class Builder
{
/**
* A map from argument names to associated query builder directives.
*
* @var ArgBuilderDirective[]
*/
protected $builderDirectives = [];
/**
* Scopes to be applied to the query builder.
*
* @var string[]
*/
protected $scopes = [];
/**
* Apply the bound QueryBuilderDirectives to the given builder.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed[] $args
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function apply($builder, array $args)
{
foreach ($args as $key => $value) {
// TODO switch to instanceof when we require bensampo/laravel-enum
// Unbox Enum values to ensure their underlying value is used for queries
if (is_a($value, '\BenSampo\Enum\Enum')) {
$value = $value->value;
}
/** @var \Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective $builderDirective */
if ($builderDirective = Arr::get($this->builderDirectives, $key)) {
$builder = $builderDirective->handleBuilder($builder, $value);
}
}
foreach ($this->scopes as $scope) {
call_user_func([$builder, $scope], $args);
}
return $builder;
}
/**
* Add scopes that are then called upon the query with the field arguments.
*
* @param string[] $scopes
* @return $this
*/
public function addScopes(array $scopes): self
{
$this->scopes = array_merge($this->scopes, $scopes);
return $this;
}
/**
* Add a query builder directive keyed by the argument name.
*
* @param string $argumentName
* @param \Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective $argBuilderDirective
* @return $this
*/
public function addBuilderDirective(string $argumentName, ArgBuilderDirective $argBuilderDirective): self
{
$this->builderDirectives[$argumentName] = $argBuilderDirective;
return $this;
}
}
@@ -0,0 +1,22 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Context;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class ContextFactory implements CreatesContext
{
/**
* Generate GraphQL context.
*
* @param \Illuminate\Http\Request $request
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function generate(Request $request): GraphQLContext
{
return new Context($request);
}
}
@@ -0,0 +1,127 @@
<?php
namespace Nuwave\Lighthouse\Execution\DataLoader;
use Exception;
use GraphQL\Deferred;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Execution\GraphQLRequest;
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;
abstract class BatchLoader
{
use HandlesCompositeKey;
/**
* Keys to resolve.
*
* @var array
*/
protected $keys = [];
/**
* Map of loaded results.
*
* [key => resolvedValue]
*
* @var mixed[]
*/
protected $results = [];
/**
* Check if data has been loaded.
*
* @var bool
*/
protected $hasLoaded = false;
/**
* Return an instance of a BatchLoader for a specific field.
*
* @param string $loaderClass The class name of the concrete BatchLoader to instantiate
* @param mixed[] $pathToField Path to the GraphQL field from the root, is used as a key for BatchLoader instances
* @param mixed[] $constructorArgs Those arguments are passed to the constructor of the new BatchLoader instance
* @return static
*
* @throws \Exception
*/
public static function instance(string $loaderClass, array $pathToField, array $constructorArgs = []): self
{
// The path to the field serves as the unique key for the instance
$instanceName = static::instanceKey($pathToField);
// If we are resolving a batched query, we need to assign each
// query a uniquely indexed instance
/** @var \Nuwave\Lighthouse\Execution\GraphQLRequest $graphQLRequest */
$graphQLRequest = app(GraphQLRequest::class);
if ($graphQLRequest->isBatched()) {
$currentBatchIndex = $graphQLRequest->batchIndex();
$instanceName = "batch_{$currentBatchIndex}_{$instanceName}";
}
// Only register a new instance if it is not already bound
$instance = app()->bound($instanceName)
? app($instanceName)
: app()->instance(
$instanceName,
app()->makeWith($loaderClass, $constructorArgs)
);
if (! $instance instanceof self) {
throw new Exception(
"The given class '$loaderClass' must resolve to an instance of Nuwave\Lighthouse\Execution\DataLoader\BatchLoader"
);
}
return $instance;
}
/**
* Generate a unique key for the instance, using the path in the query.
*
* @param mixed[] $path
* @return string
*/
public static function instanceKey(array $path): string
{
return (new Collection($path))
->filter(function ($path): bool {
// Ignore numeric path entries, as those signify an array of fields.
// Combining the queries for those is the very purpose of the
// batch loader, so they must not be included.
return ! is_numeric($path);
})
->implode('_');
}
/**
* Load object by key.
*
* @param mixed $key
* @param mixed[] $metaInfo
* @return \GraphQL\Deferred
*/
public function load($key, array $metaInfo = []): Deferred
{
$key = $this->buildKey($key);
$this->keys[$key] = $metaInfo;
return new Deferred(function () use ($key) {
if (! $this->hasLoaded) {
$this->results = $this->resolve();
$this->hasLoaded = true;
}
return $this->results[$key];
});
}
/**
* Resolve the keys.
*
* The result has to be an associative array: [key => result]
*
* @return mixed[]
*/
abstract public function resolve(): array;
}
@@ -0,0 +1,410 @@
<?php
namespace Nuwave\Lighthouse\Execution\DataLoader;
use Closure;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Relations\Relation;
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
class ModelRelationFetcher
{
use HandlesCompositeKey;
/**
* The parent models that relations should be loaded for.
*
* @var \Illuminate\Database\Eloquent\Collection
*/
protected $models;
/**
* The relations to be loaded. Same format as the `with` method in Eloquent builder.
*
* @var mixed[]
*/
protected $relations;
/**
* @param mixed $models The parent models that relations should be loaded for
* @param mixed[] $relations The relations to be loaded. Same format as the `with` method in Eloquent builder.
* @return void
*/
public function __construct($models, array $relations)
{
$this->setModels($models);
$this->setRelations($relations);
}
/**
* Set the relations to be loaded.
*
* @param array $relations
* @return $this
*/
public function setRelations(array $relations): self
{
// Parse and set the relations.
$this->relations = $this->newModelQuery()
->with($relations)
->getEagerLoads();
return $this;
}
/**
* Return a fresh instance of a query builder for the underlying model.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newModelQuery(): EloquentBuilder
{
return $this->models()
->first()
->newModelQuery();
}
/**
* Get all the underlying models.
*
* @return \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>
*/
public function models(): EloquentCollection
{
return $this->models;
}
/**
* Set one or more Model instances as an EloquentCollection.
*
* @param mixed $models
* @return $this
*/
protected function setModels($models): self
{
$this->models = $models instanceof EloquentCollection
? $models
: new EloquentCollection($models);
return $this;
}
/**
* Load all the relations of all the models.
*
* @return $this
*/
public function loadRelations(): self
{
$this->models->load($this->relations);
return $this;
}
/**
* Load all relations for the model, but constrain the query to the current page.
*
* @param int $perPage
* @param int $page
* @return $this
*/
public function loadRelationsForPage(int $perPage, int $page = 1): self
{
foreach ($this->relations as $name => $constraints) {
$this->loadRelationForPage($perPage, $page, $name, $constraints);
}
return $this;
}
/**
* Load only one page of relations of all the models.
*
* The relation will be converted to a `Paginator` instance.
*
* @param int $first
* @param int $page
* @param string $relationName
* @param \Closure $relationConstraints
* @return $this
*/
public function loadRelationForPage(int $first, int $page, string $relationName, Closure $relationConstraints): self
{
// Load the count of relations of models, this will be the `total` argument of `Paginator`.
// Be aware that this will reload all the models entirely with the count of their relations,
// which will bring extra DB queries, always prefer querying without pagination if possible.
$this->reloadModelsWithRelationCount();
$relations = $this
->buildRelationsFromModels($relationName, $relationConstraints)
->map(
function (Relation $relation) use ($first, $page) {
return $relation->forPage($page, $first);
}
);
/** @var \Illuminate\Database\Eloquent\Collection $relationModels */
$relationModels = $this
->unionAllRelationQueries($relations)
->get();
$this->hydratePivotRelation($relationName, $relationModels);
$this->loadDefaultWith($relationModels);
$this->associateRelationModels($relationName, $relationModels);
$this->convertRelationToPaginator($first, $page, $relationName);
return $this;
}
/**
* Reload the models to get the `{relation}_count` attributes of models set.
*
* @return $this
*/
public function reloadModelsWithRelationCount(): self
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = $this->models()
->first()
->newQuery()
->withCount($this->relations);
$ids = $this->getModelIds();
$reloadedModels = $query
->whereKey($ids)
->get()
->filter(function (Model $model) use ($ids): bool {
return in_array(
$model->getKey(),
$ids,
true
);
});
return $this->setModels($reloadedModels);
}
/**
* Extract the primary keys from the underlying models.
*
* @return mixed[]
*/
protected function getModelIds(): array
{
return $this->models
->map(function (Model $model) {
return $model->getKey();
})
->all();
}
/**
* Get queries to fetch relationships.
*
* @param string $relationName
* @param \Closure $relationConstraints
* @return \Illuminate\Support\Collection<\Illuminate\Database\Eloquent\Relations\Relation>
*/
protected function buildRelationsFromModels(string $relationName, Closure $relationConstraints): Collection
{
return $this->models->toBase()->map(
function (Model $model) use ($relationName, $relationConstraints): Relation {
$relation = $this->getRelationInstance($relationName);
$relation->addEagerConstraints([$model]);
// Call the constraints
$relationConstraints($relation, $model);
if (method_exists($relation, 'shouldSelect')) {
$shouldSelect = new ReflectionMethod(get_class($relation), 'shouldSelect');
$shouldSelect->setAccessible(true);
$select = $shouldSelect->invoke($relation, ['*']);
$relation->addSelect($select);
} elseif (method_exists($relation, 'getSelectColumns')) {
$getSelectColumns = new ReflectionMethod(get_class($relation), 'getSelectColumns');
$getSelectColumns->setAccessible(true);
$select = $getSelectColumns->invoke($relation, ['*']);
$relation->addSelect($select);
}
$relation->initRelation([$model], $relationName);
return $relation;
}
);
}
/**
* Load default eager loads.
*
* @param \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $collection
* @return $this
*/
protected function loadDefaultWith(EloquentCollection $collection): self
{
if ($collection->isEmpty()) {
return $this;
}
$model = $collection->first();
$reflection = new ReflectionClass($model);
$withProperty = $reflection->getProperty('with');
$withProperty->setAccessible(true);
$with = array_filter((array) $withProperty->getValue($model), function ($relation) use ($model): bool {
return ! $model->relationLoaded($relation);
});
if (! empty($with)) {
$collection->load($with);
}
return $this;
}
/**
* This is the name that Eloquent gives to the attribute that contains the count.
*
* @see \Illuminate\Database\Eloquent\Concerns\QueriesRelationships->withCount()
*
* @param string $relationName
* @return string
*/
public function getRelationCountName(string $relationName): string
{
return Str::snake("{$relationName}_count");
}
/**
* Get an associative array of relations, keyed by the models primary key.
*
* @param string $relationName
* @return mixed[]
*/
public function getRelationDictionary(string $relationName): array
{
return $this->models
->mapWithKeys(
function (Model $model) use ($relationName): array {
return [$this->buildKey($model->getKey()) => $model->getRelation($relationName)];
}
)->all();
}
/**
* Merge all the relation queries into a single query with UNION ALL.
*
* @param \Illuminate\Support\Collection $relations
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function unionAllRelationQueries(Collection $relations): EloquentBuilder
{
return $relations
->reduce(
function (EloquentBuilder $builder, Relation $relation) {
return $builder->unionAll(
$relation->getQuery()
);
},
// Use the first query as the initial starting point
$relations->shift()->getQuery()
);
}
/**
* @param int $first
* @param int $page
* @param string $relationName
* @return $this
*/
protected function convertRelationToPaginator(int $first, int $page, string $relationName): self
{
$this->models->each(function (Model $model) use ($page, $first, $relationName): void {
$total = $model->getAttribute(
$this->getRelationCountName($relationName)
);
$paginator = app()->makeWith(
LengthAwarePaginator::class,
[
'items' => $model->getRelation($relationName),
'total' => $total,
'perPage' => $first,
'currentPage' => $page,
'options' => [],
]
);
$model->setRelation($relationName, $paginator);
});
return $this;
}
/**
* Associate the collection of all fetched relationModels back with their parents.
*
* @param string $relationName
* @param \Illuminate\Database\Eloquent\Collection $relationModels
* @return $this
*/
protected function associateRelationModels(string $relationName, EloquentCollection $relationModels): self
{
$relation = $this->getRelationInstance($relationName);
$relation->match(
$this->models->all(),
$relationModels,
$relationName
);
return $this;
}
/**
* Ensure the pivot relation is hydrated too, if it exists.
*
* @param string $relationName
* @param \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $relationModels
* @return $this
*/
protected function hydratePivotRelation(string $relationName, EloquentCollection $relationModels): self
{
$relation = $this->getRelationInstance($relationName);
if ($relationModels->isNotEmpty() && method_exists($relation, 'hydratePivotRelation')) {
$hydrationMethod = new ReflectionMethod(get_class($relation), 'hydratePivotRelation');
$hydrationMethod->setAccessible(true);
$hydrationMethod->invoke($relation, $relationModels->all());
}
return $this;
}
/**
* Use the underlying model to instantiate a relation by name.
*
* @param string $relationName
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
protected function getRelationInstance(string $relationName): Relation
{
return $this
->newModelQuery()
->getRelation($relationName);
}
}
@@ -0,0 +1,122 @@
<?php
namespace Nuwave\Lighthouse\Execution\DataLoader;
use Illuminate\Support\Collection;
use GraphQL\Type\Definition\ResolveInfo;
class RelationBatchLoader extends BatchLoader
{
/**
* The name of the Eloquent relation to load.
*
* @var string
*/
protected $relationName;
/**
* The arguments that were passed to the field.
*
* @var mixed[]
*/
protected $args;
/**
* Names of the scopes that have to be called for the query.
*
* @var string[]
*/
protected $scopes;
/**
* The ResolveInfo of the currently executing field.
*
* @var \GraphQL\Type\Definition\ResolveInfo
*/
protected $resolveInfo;
/**
* Present when using pagination, the amount of rows to be fetched.
*
* @var int|null
*/
protected $first;
/**
* Present when using pagination, the page to be fetched.
*
* @var int|null
*/
protected $page;
/**
* @param string $relationName
* @param mixed[] $args
* @param string[] $scopes
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @param int|null $first
* @param int|null $page
* @return void
*/
public function __construct(
string $relationName,
array $args,
array $scopes,
ResolveInfo $resolveInfo,
?int $first = null,
?int $page = null
) {
$this->relationName = $relationName;
$this->args = $args;
$this->scopes = $scopes;
$this->resolveInfo = $resolveInfo;
$this->first = $first;
$this->page = $page;
}
/**
* Resolve the keys.
*
* @return mixed[]
*/
public function resolve(): array
{
$modelRelationFetcher = $this->getRelationFetcher();
if ($this->first !== null) {
$modelRelationFetcher->loadRelationsForPage($this->first, $this->page);
} else {
$modelRelationFetcher->loadRelations();
}
return $modelRelationFetcher->getRelationDictionary($this->relationName);
}
/**
* Construct a new instance of a relation fetcher.
*
* @return \Nuwave\Lighthouse\Execution\DataLoader\ModelRelationFetcher
*/
protected function getRelationFetcher(): ModelRelationFetcher
{
return new ModelRelationFetcher(
$this->getParentModels(),
[$this->relationName => function ($query) {
return $this->resolveInfo
->builder
->addScopes($this->scopes)
->apply($query, $this->args);
}]
);
}
/**
* Get the parents from the keys that are present on the BatchLoader.
*
* @return \Illuminate\Support\Collection<\Illuminate\Database\Eloquent\Model>
*/
protected function getParentModels(): Collection
{
return (new Collection($this->keys))->pluck('parent');
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Closure;
use Nuwave\Lighthouse\Exceptions\GenericException;
class ErrorBuffer
{
/**
* The gathered error messages.
*
* @var string[]
*/
protected $errors = [];
/**
* @var string
*/
protected $errorType;
/**
* @var \Closure
*/
protected $exceptionResolver;
/**
* ErrorBuffer constructor.
*
* @param string $errorType
* @param \Closure|null $exceptionResolver
* @return void
*/
public function __construct(string $errorType = 'generic', ?Closure $exceptionResolver = null)
{
$this->errorType = $errorType;
$this->exceptionResolver = $exceptionResolver ?? $this->defaultExceptionResolver();
}
/**
* Construct a default exception resolver.
*
* @return \Closure
*/
protected function defaultExceptionResolver(): Closure
{
return function (string $errorMessage): GenericException {
return (new GenericException($errorMessage))
->setExtensions([$this->errorType => $this->errors])
->setCategory($this->errorType);
};
}
/**
* Set the Exception resolver.
*
* @param \Closure $exceptionResolver
* @return $this
*/
public function setExceptionResolver(Closure $exceptionResolver): self
{
$this->exceptionResolver = $exceptionResolver;
return $this;
}
/**
* Resolve the exception by calling the exception handler with the given args.
*
* @param mixed ...$args
* @return mixed
*/
protected function resolveException(...$args)
{
return ($this->exceptionResolver)(...$args);
}
/**
* Push an error message into the buffer.
*
* @param string $errorMessage
* @param string|null $key
* @return $this
*/
public function push(string $errorMessage, ?string $key = null): self
{
if ($key === null) {
$this->errors[] = $errorMessage;
} else {
$this->errors[$key][] = $errorMessage;
}
return $this;
}
/**
* Flush the errors.
*
* @param string $errorMessage
* @return void
*
* @throws \Exception
*/
public function flush(string $errorMessage): void
{
if (! $this->hasErrors()) {
return;
}
$exception = $this->resolveException($errorMessage, $this);
$this->clearErrors();
throw $exception;
}
/**
* Reset the errors to an empty array.
*
* @return void
*/
public function clearErrors(): void
{
$this->errors = [];
}
/**
* Get the error type.
*
* @return string
*/
public function errorType(): string
{
return $this->errorType;
}
/**
* Set the error type.
*
* @param string $errorType
* @return $this
*/
public function setErrorType(string $errorType): self
{
$this->errorType = $errorType;
return $this;
}
/**
* Have we encountered any errors yet?
*
* @return bool
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Closure;
use GraphQL\Error\Error;
interface ErrorHandler
{
/**
* This function receives all GraphQL errors and may alter them or do something else with them.
*
* Always call $next($error) to keep the Pipeline going. Multiple such Handlers may be registered
* as an array in the config.
*
* @param \GraphQL\Error\Error $error
* @param \Closure $next
* @return array
*/
public static function handle(Error $error, Closure $next): array;
}
@@ -0,0 +1,39 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Closure;
use GraphQL\Error\Error;
use Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions;
class ExtensionErrorHandler implements ErrorHandler
{
/**
* Handle Exceptions that implement Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions
* and add extra content from them to the 'extensions' key of the Error that is rendered
* to the User.
*
* @param \GraphQL\Error\Error $error
* @param \Closure $next
* @return array
*/
public static function handle(Error $error, Closure $next): array
{
$underlyingException = $error->getPrevious();
if ($underlyingException instanceof RendersErrorsExtensions) {
// Reconstruct the error, passing in the extensions of the underlying exception
$error = new Error(
$error->message,
$error->nodes,
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$underlyingException,
$underlyingException->extensionsContent()
);
}
return $next($error);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Nuwave\Lighthouse\Execution;
/**
* May be returned from listeners of the event:.
* @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse
*/
class ExtensionsResponse
{
/**
* Will be used as the key in the response map.
*
* @var string
*/
protected $key;
/**
* JSON-encodable content of the extension.
*
* @var mixed
*/
protected $content;
/**
* ExtensionsResponse constructor.
*
* @param string $key
* @param mixed $content
* @return void
*/
public function __construct(string $key, $content)
{
$this->key = $key;
$this->content = $content;
}
/**
* Return the key of the extension.
*
* @return string
*/
public function key(): string
{
return $this->key;
}
/**
* Return the JSON-encodable content of the extension.
*
* @return mixed
*/
public function content()
{
return $this->content;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Nuwave\Lighthouse\Execution;
interface GraphQLRequest
{
/**
* Get the contained GraphQL query string.
*
* @return string
*/
public function query(): string;
/**
* Get the given variables for the query.
*
* @return mixed[]
*/
public function variables(): array;
/**
* Get the operationName of the current request.
*
* @return string|null
*/
public function operationName(): ?string;
/**
* Is the current query a batched query?
*
* @return bool
*/
public function isBatched(): bool;
/**
* Advance the batch index and indicate if there are more batches to process.
*
* @return bool
*/
public function advanceBatchIndex(): bool;
/**
* Get the index of the current batch.
*
* Returns null if we are not resolving a batched query.
*
* @return int|null
*/
public function batchIndex(): ?int;
}
@@ -0,0 +1,54 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class GraphQLValidator extends Validator
{
/**
* Get the root object that was passed to the field that is being validated.
*
* @return mixed
*/
public function getRoot()
{
return Arr::get($this->customAttributes, 'root');
}
/**
* Get the context that was passed to the field that is being validated.
*
* @return \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public function getContext(): GraphQLContext
{
return Arr::get($this->customAttributes, 'context');
}
/**
* Get the resolve info that was passed to the field that is being validated.
*
* @return \GraphQL\Type\Definition\ResolveInfo
*/
public function getResolveInfo(): ResolveInfo
{
return Arr::get($this->customAttributes, 'resolveInfo');
}
/**
* Return the dot separated path of the field that is being validated.
*
* @return string
*/
public function getFieldPath(): string
{
return implode(
'.',
$this->getResolveInfo()->path
);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Http\Request;
class LighthouseRequest extends BaseRequest
{
/**
* The incoming HTTP request.
*
* @var \Illuminate\Http\Request
*/
protected $request;
/**
* LighthouseRequest constructor.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(Request $request)
{
$this->request = $request;
// If the request has neither a query, nor an operationName,
// we assume we are resolving a batched query.
if (! $request->hasAny('query', 'operationName')) {
$this->batchIndex = 0;
}
}
/**
* Get the given variables for the query.
*
* @return mixed[]
*/
public function variables(): array
{
$variables = $this->fieldValue('variables');
// In case we are resolving a GET request, variables
// are sent as a JSON encoded string
if (is_string($variables)) {
return json_decode($variables, true) ?? [];
}
// If this is a POST request, Laravel already decoded the input for us
return $variables ?? [];
}
/**
* Are there more batched queries to process?
*
* @return bool
*/
protected function hasMoreBatches(): bool
{
return count($this->request->input()) - 1 > $this->batchIndex;
}
/**
* Get the contents of a field by key.
*
* This is expected to take batched requests into consideration.
*
* @param string $key
* @return array|string|null
*/
protected function fieldValue(string $key)
{
return $this->request->input($key)
?? $this->request->input("{$this->batchIndex}.{$key}");
}
}
@@ -0,0 +1,93 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use GraphQL\Error\InvariantViolation;
class MultipartFormRequest extends BaseRequest
{
/**
* One or more operations, consisting of query, variables and operationName.
*
* https://github.com/jaydenseric/graphql-multipart-request-spec#single-file
*
* @var mixed[]
*/
protected $operations;
/**
* MultipartFormRequest constructor.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(Request $request)
{
if (! $request->has('map')) {
throw new InvariantViolation(
'Could not find a valid map, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
);
}
$this->operations = json_decode(
$request->input('operations'),
true
);
// If operations is 0-indexed, we assume we are resolving a batched query
if (isset($this->operations[0])) {
$this->batchIndex = 0;
}
$map = json_decode($request->input('map'), true);
/**
* @var string
* @var array $operationsPaths
*/
foreach ($map as $fileKey => $operationsPaths) {
$file = $request->file($fileKey);
/** @var string $operationsPath */
foreach ($operationsPaths as $operationsPath) {
Arr::set($this->operations, $operationsPath, $file);
}
}
}
/**
* Get the given variables for the query.
*
* @return mixed[]
*/
public function variables(): array
{
return $this->fieldValue('variables') ?? [];
}
/**
* If we are dealing with a batched request, this gets the
* contents of the currently resolving batch index.
*
* @param string $key
* @return array|string|null
*/
protected function fieldValue(string $key)
{
return $this->isBatched()
? Arr::get($this->operations, $this->batchIndex.'.'.$key)
: $this->operations[$key] ?? null;
}
/**
* Are there more batched queries to process?
*
* @return bool
*/
protected function hasMoreBatches(): bool
{
return count($this->operations) - 1 > $this->batchIndex;
}
}
@@ -0,0 +1,426 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use ReflectionClass;
use ReflectionNamedType;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class MutationExecutor
{
/**
* Execute a create mutation.
*
* @param \Illuminate\Database\Eloquent\Model $model
* An empty instance of the model that should be created
* @param \Illuminate\Support\Collection $args
* The corresponding slice of the input arguments for creating this model
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
* If we are in a nested create, we can use this to associate the new model to its parent
* @return \Illuminate\Database\Eloquent\Model
*/
public static function executeCreate(Model $model, Collection $args, ?Relation $parentRelation = null): Model
{
$reflection = new ReflectionClass($model);
[$hasMany, $remaining] = self::partitionArgsByRelationType($reflection, $args, HasMany::class);
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class);
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class);
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class);
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class);
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class);
$model = self::saveModelWithPotentialParent($model, $remaining, $parentRelation);
$createOneToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
};
$hasMany->each($createOneToMany);
$morphMany->each($createOneToMany);
$createOneToOne = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleSingleRelationCreate(new Collection($nestedOperations['create']), $relation);
}
};
$hasOne->each($createOneToOne);
$morphOne->each($createOneToOne);
$createManyToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany|\Illuminate\Database\Eloquent\Relations\MorphToMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['sync'])) {
$relation->sync($nestedOperations['sync']);
}
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['connect'])) {
$relation->attach($nestedOperations['connect']);
}
};
$belongsToMany->each($createManyToMany);
$morphToMany->each($createManyToMany);
return $model;
}
/**
* Save a model that maybe has a parent.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Support\Collection $args
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
* @return \Illuminate\Database\Eloquent\Model
*/
protected static function saveModelWithPotentialParent(Model $model, Collection $args, ?Relation $parentRelation = null): Model
{
$reflection = new ReflectionClass($model);
// Extract $morphTo first, as MorphTo extends BelongsTo
[$morphTo, $remaining] = self::partitionArgsByRelationType(
$reflection,
$args,
MorphTo::class
);
[$belongsTo, $remaining] = self::partitionArgsByRelationType(
$reflection,
$remaining,
BelongsTo::class
);
// Use all the remaining attributes and fill the model
$model->fill(
$remaining->all()
);
$belongsTo->each(function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsTo $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
$belongsToModel = self::executeCreate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['create'])
);
$relation->associate($belongsToModel);
}
if (isset($nestedOperations['connect'])) {
$relation->associate($nestedOperations['connect']);
}
if (isset($nestedOperations['update'])) {
$belongsToModel = self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['update'])
);
$relation->associate($belongsToModel);
}
// We proceed with disconnecting/deleting only if the given $values is truthy.
// There is no other information to be passed when issuing those operations,
// but GraphQL forces us to pass some value. It would be unintuitive for
// the end user if the given value had no effect on the execution.
if ($nestedOperations['disconnect'] ?? false) {
$relation->dissociate();
}
if ($nestedOperations['delete'] ?? false) {
$relation->delete();
}
});
$morphTo->each(function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\MorphTo $relation */
$relation = $model->{$relationName}();
// TODO implement create and update once we figure out how to do polymorphic input types https://github.com/nuwave/lighthouse/issues/900
if (isset($nestedOperations['connect'])) {
$connectArgs = $nestedOperations['connect'];
$morphToModel = $relation->createModelByType(
(string) $connectArgs['type']
);
$morphToModel->setAttribute(
$morphToModel->getKeyName(),
$connectArgs['id']
);
$relation->associate($morphToModel);
}
// We proceed with disconnecting/deleting only if the given $values is truthy.
// There is no other information to be passed when issuing those operations,
// but GraphQL forces us to pass some value. It would be unintuitive for
// the end user if the given value had no effect on the execution.
if ($nestedOperations['disconnect'] ?? false) {
$relation->dissociate();
}
if ($nestedOperations['delete'] ?? false) {
$relation->delete();
}
});
if ($parentRelation && ! $parentRelation instanceof BelongsToMany) {
// If we are already resolving a nested create, we might
// already have an instance of the parent relation available.
// In that case, use it to set the current model as a child.
$parentRelation->save($model);
return $model;
}
$model->save();
if ($parentRelation instanceof BelongsToMany) {
$parentRelation->syncWithoutDetaching($model);
}
return $model;
}
/**
* Handle the creation with multiple relations.
*
* @param \Illuminate\Support\Collection $multiValues
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @return void
*/
protected static function handleMultiRelationCreate(Collection $multiValues, Relation $relation): void
{
$multiValues->each(function ($singleValues) use ($relation): void {
self::handleSingleRelationCreate(new Collection($singleValues), $relation);
});
}
/**
* Handle the creation with a single relation.
*
* @param \Illuminate\Support\Collection $singleValues
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @return void
*/
protected static function handleSingleRelationCreate(Collection $singleValues, Relation $relation): void
{
self::executeCreate(
$relation->getModel()->newInstance(),
$singleValues,
$relation
);
}
/**
* Execute an update mutation.
*
* @param \Illuminate\Database\Eloquent\Model $model
* An empty instance of the model that should be updated
* @param \Illuminate\Support\Collection $args
* The corresponding slice of the input arguments for updating this model
* @param \Illuminate\Database\Eloquent\Relations\Relation|null $parentRelation
* If we are in a nested update, we can use this to associate the new model to its parent
* @return \Illuminate\Database\Eloquent\Model
*/
public static function executeUpdate(Model $model, Collection $args, ?Relation $parentRelation = null): Model
{
$id = $args->pull('id')
?? $args->pull(
$model->getKeyName()
);
$model = $model->newQuery()->findOrFail($id);
$reflection = new ReflectionClass($model);
[$hasMany, $remaining] = self::partitionArgsByRelationType($reflection, $args, HasMany::class);
[$morphMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphMany::class);
[$hasOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, HasOne::class);
[$morphOne, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphOne::class);
[$belongsToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, BelongsToMany::class);
[$morphToMany, $remaining] = self::partitionArgsByRelationType($reflection, $remaining, MorphToMany::class);
$model = self::saveModelWithPotentialParent($model, $remaining, $parentRelation);
$updateOneToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['delete'])) {
$relation->getModel()::destroy($nestedOperations['delete']);
}
};
$hasMany->each($updateOneToMany);
$morphMany->each($updateOneToMany);
$updateOneToOne = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['create'])) {
self::handleSingleRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($nestedOperations['update']),
$relation
);
}
if (isset($nestedOperations['delete'])) {
$relation->getModel()::destroy($nestedOperations['delete']);
}
};
$hasOne->each($updateOneToOne);
$morphOne->each($updateOneToOne);
$updateManyToMany = function (array $nestedOperations, string $relationName) use ($model): void {
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany|\Illuminate\Database\Eloquent\Relations\MorphToMany $relation */
$relation = $model->{$relationName}();
if (isset($nestedOperations['sync'])) {
$relation->sync($nestedOperations['sync']);
}
if (isset($nestedOperations['create'])) {
self::handleMultiRelationCreate(new Collection($nestedOperations['create']), $relation);
}
if (isset($nestedOperations['update'])) {
(new Collection($nestedOperations['update']))->each(function ($singleValues) use ($relation): void {
self::executeUpdate(
$relation->getModel()->newInstance(),
new Collection($singleValues),
$relation
);
});
}
if (isset($nestedOperations['delete'])) {
$relation->detach($nestedOperations['delete']);
$relation->getModel()::destroy($nestedOperations['delete']);
}
if (isset($nestedOperations['connect'])) {
$relation->attach($nestedOperations['connect']);
}
if (isset($nestedOperations['disconnect'])) {
$relation->detach($nestedOperations['disconnect']);
}
};
$belongsToMany->each($updateManyToMany);
$morphToMany->each($updateManyToMany);
return $model;
}
/**
* Extract all the arguments that correspond to a relation of a certain type on the model.
*
* For example, if the args input looks like this:
*
* [
* 'comments' =>
* ['foo' => 'Bar'],
* 'name' => 'Ralf',
* ]
*
* and the model has a method "comments" that returns a HasMany relationship,
* the result will be:
* [
* [
* 'comments' =>
* ['foo' => 'Bar'],
* ],
* [
* 'name' => 'Ralf',
* ]
* ]
*
* @param \ReflectionClass $modelReflection
* @param \Illuminate\Support\Collection $args
* @param string $relationClass
* @return \Illuminate\Support\Collection [relationshipArgs, remainingArgs]
*/
protected static function partitionArgsByRelationType(ReflectionClass $modelReflection, Collection $args, string $relationClass): Collection
{
return $args->partition(
function ($value, string $key) use ($modelReflection, $relationClass): bool {
if (! $modelReflection->hasMethod($key)) {
return false;
}
$relationMethodCandidate = $modelReflection->getMethod($key);
if (! $returnType = $relationMethodCandidate->getReturnType()) {
return false;
}
if (! $returnType instanceof ReflectionNamedType) {
return false;
}
return is_a($returnType->getName(), $relationClass, true);
}
);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use GraphQL\Language\Parser;
use GraphQL\Language\AST\Node;
use Illuminate\Support\Collection;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
class QueryAST
{
/**
* The definitions contained in the AST of an incoming query.
*
* @var \Illuminate\Support\Collection
*/
protected $definitions;
/**
* @param \GraphQL\Language\AST\DocumentNode $documentNode
* @return void
*/
public function __construct(DocumentNode $documentNode)
{
$this->definitions = new Collection($documentNode->definitions);
}
/**
* Create a new instance from a query string.
*
* @param string $query
* @return static
*/
public static function fromSource(string $query): self
{
return new static(
Parser::parse($query)
);
}
/**
* Get all operation definitions.
*
* @return \Illuminate\Support\Collection<\GraphQL\Language\AST\OperationDefinitionNode>
*/
public function operationDefinitions(): Collection
{
return $this->definitionsByType(OperationDefinitionNode::class);
}
/**
* Get all fragment definitions.
*
* @return \Illuminate\Support\Collection<\GraphQL\Language\AST\FragmentDefinitionNode>
*/
public function fragmentDefinitions(): Collection
{
return $this->definitionsByType(FragmentDefinitionNode::class);
}
/**
* Get all definitions of a given type.
*
* @param string $typeClassName
* @return \Illuminate\Support\Collection
*/
protected function definitionsByType(string $typeClassName): Collection
{
return $this->definitions
->filter(function (Node $node) use ($typeClassName): bool {
return $node instanceof $typeClassName;
});
}
}
@@ -0,0 +1,20 @@
<?php
namespace Nuwave\Lighthouse\Execution;
use Symfony\Component\HttpFoundation\Response;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
class SingleResponse implements CreatesResponse
{
/**
* Create a HTTP response from the final result.
*
* @param mixed[] $result
* @return \Symfony\Component\HttpFoundation\Response
*/
public function createResponse(array $result): Response
{
return response($result);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Nuwave\Lighthouse\Execution\Utils;
use Nuwave\Lighthouse\Support\Contracts\GlobalId as GlobalIdContract;
/**
* The default encoding of global IDs in Lighthouse.
*
* The way that IDs are generated basically works like this:
*
* 1. Take the name of a type, e.g. "User" and an ID, e.g. 123
* 2. Glue them together, separated by a colon, e.g. "User:123"
* 3. base64_encode the result
*
* This can then be reversed to uniquely identify an entity in our
* schema, just by looking at a single ID.
*/
class GlobalId implements GlobalIdContract
{
/**
* Glue together a type and an id to create a global id.
*
* @param string $type
* @param string|int $id
* @return string
*/
public function encode(string $type, $id): string
{
return base64_encode($type.':'.$id);
}
/**
* Split a global id into the type and the id it contains.
*
* @param string $globalID
* @return array Contains [$type, $id], e.g. ['User', '123']
*/
public function decode(string $globalID): array
{
return explode(':', base64_decode($globalID));
}
/**
* Decode the Global ID and get just the ID.
*
* @param string $globalID
* @return string
*/
public function decodeID(string $globalID): string
{
[$type, $id] = self::decode($globalID);
return $id;
}
/**
* Decode the Global ID and get just the type.
*
* @param string $globalID
* @return string
*/
public function decodeType(string $globalID): string
{
[$type, $id] = self::decode($globalID);
return $type;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Nuwave\Lighthouse\Execution\Utils;
use Throwable;
use InvalidArgumentException;
use Nuwave\Lighthouse\GraphQL;
use Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry;
use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler;
class Subscription
{
/**
* Broadcast subscription to client(s).
*
* @param string $subscriptionField
* @param mixed $root
* @param bool|null $shouldQueue
* @return void
*
* @throws \InvalidArgumentException
*/
public static function broadcast(string $subscriptionField, $root, ?bool $shouldQueue = null): void
{
// Ensure we have a schema and registered subscription fields
// in the event we are calling this method in code.
/** @var \Nuwave\Lighthouse\GraphQL $graphQL */
$graphQL = app(GraphQL::class);
$graphQL->prepSchema();
/** @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $registry */
$registry = app(SubscriptionRegistry::class);
if (! $registry->has($subscriptionField)) {
throw new InvalidArgumentException("No subscription field registered for {$subscriptionField}");
}
/** @var \Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions $broadcaster */
$broadcaster = app(BroadcastsSubscriptions::class);
$shouldQueue = $shouldQueue === null
? config('lighthouse.subscriptions.queue_broadcasts', false)
: $shouldQueue;
$method = $shouldQueue
? 'queueBroadcast'
: 'broadcast';
try {
call_user_func(
[$broadcaster, $method],
$registry->subscription($subscriptionField),
$subscriptionField,
$root
);
} catch (Throwable $e) {
/** @var \Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionExceptionHandler $exceptionHandler */
$exceptionHandler = app(SubscriptionExceptionHandler::class);
$exceptionHandler->handleBroadcastError($e);
}
}
}
+271
View File
@@ -0,0 +1,271 @@
<?php
namespace Nuwave\Lighthouse;
use GraphQL\Error\Error;
use GraphQL\Type\Schema;
use GraphQL\GraphQL as GraphQLBase;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Validator\Rules\QueryDepth;
use Nuwave\Lighthouse\Support\Pipeline;
use GraphQL\Validator\DocumentValidator;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use GraphQL\Validator\Rules\QueryComplexity;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Events\ManipulateResult;
use Nuwave\Lighthouse\Execution\GraphQLRequest;
use GraphQL\Validator\Rules\DisableIntrospection;
use Nuwave\Lighthouse\Events\BuildExtensionsResponse;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
class GraphQL
{
/**
* The executable schema.
*
* @var \GraphQL\Type\Schema
*/
protected $executableSchema;
/**
* The parsed schema AST.
*
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
protected $documentAST;
/**
* The schema builder.
*
* @var \Nuwave\Lighthouse\Schema\SchemaBuilder
*/
protected $schemaBuilder;
/**
* The pipeline.
*
* @var \Nuwave\Lighthouse\Support\Pipeline
*/
protected $pipeline;
/**
* The event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $eventDispatcher;
/**
* The AST builder.
*
* @var \Nuwave\Lighthouse\Schema\AST\ASTBuilder
*/
protected $astBuilder;
/**
* The context factory.
*
* @var \Nuwave\Lighthouse\Support\Contracts\CreatesContext
*/
protected $createsContext;
/**
* GraphQL constructor.
*
* @param \Nuwave\Lighthouse\Schema\SchemaBuilder $schemaBuilder
* @param \Nuwave\Lighthouse\Support\Pipeline $pipeline
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
* @param \Nuwave\Lighthouse\Schema\AST\ASTBuilder $astBuilder
* @param \Nuwave\Lighthouse\Support\Contracts\CreatesContext $createsContext
* @return void
*/
public function __construct(
SchemaBuilder $schemaBuilder,
Pipeline $pipeline,
EventDispatcher $eventDispatcher,
ASTBuilder $astBuilder,
CreatesContext $createsContext
) {
$this->schemaBuilder = $schemaBuilder;
$this->pipeline = $pipeline;
$this->eventDispatcher = $eventDispatcher;
$this->astBuilder = $astBuilder;
$this->createsContext = $createsContext;
}
/**
* Execute a set of batched queries on the lighthouse schema and return a
* collection of ExecutionResults.
*
* @param \Nuwave\Lighthouse\Execution\GraphQLRequest $request
* @return mixed[]
*/
public function executeRequest(GraphQLRequest $request): array
{
$result = $this->executeQuery(
$request->query(),
$this->createsContext->generate(
app('request')
),
$request->variables(),
null,
$request->operationName()
);
return $this->applyDebugSettings($result);
}
/**
* Apply the debug settings from the config and get the result as an array.
*
* @param \GraphQL\Executor\ExecutionResult $result
* @return mixed[]
*/
public function applyDebugSettings(ExecutionResult $result): array
{
// If debugging is set to false globally, do not add GraphQL specific
// debugging info either. If it is true, then we fetch the debug
// level from the Lighthouse configuration.
return $result->toArray(
config('app.debug')
? config('lighthouse.debug')
: false
);
}
/**
* Execute a GraphQL query on the Lighthouse schema and return the raw ExecutionResult.
*
* To render the ExecutionResult, you will probably want to call `->toArray($debug)` on it,
* with $debug being a combination of flags in \GraphQL\Error\Debug
*
* @param string|\GraphQL\Language\AST\DocumentNode $query
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param mixed[] $variables
* @param mixed|null $rootValue
* @param string|null $operationName
* @return \GraphQL\Executor\ExecutionResult
*/
public function executeQuery(
$query,
GraphQLContext $context,
?array $variables = [],
$rootValue = null,
?string $operationName = null
): ExecutionResult {
// Building the executable schema might take a while to do,
// so we do it before we fire the StartExecution event.
// This allows tracking the time for batched queries independently.
$this->prepSchema();
$this->eventDispatcher->dispatch(
new StartExecution
);
$result = GraphQLBase::executeQuery(
$this->executableSchema,
$query,
$rootValue,
$context,
$variables,
$operationName,
null,
$this->getValidationRules() + DocumentValidator::defaultRules()
);
/** @var \Nuwave\Lighthouse\Execution\ExtensionsResponse[] $extensionsResponses */
$extensionsResponses = (array) $this->eventDispatcher->dispatch(
new BuildExtensionsResponse
);
foreach ($extensionsResponses as $extensionsResponse) {
if ($extensionsResponse) {
$result->extensions[$extensionsResponse->key()] = $extensionsResponse->content();
}
}
$result->setErrorsHandler(
function (array $errors, callable $formatter): array {
// User defined error handlers, implementing \Nuwave\Lighthouse\Execution\ErrorHandler
// This allows the user to register multiple handlers and pipe the errors through.
$handlers = config('lighthouse.error_handlers', []);
return array_map(
function (Error $error) use ($handlers, $formatter) {
return $this->pipeline
->send($error)
->through($handlers)
->then(function (Error $error) use ($formatter) {
return $formatter($error);
});
},
$errors
);
}
);
// Allow listeners to manipulate the result after each resolved query
$this->eventDispatcher->dispatch(
new ManipulateResult($result)
);
return $result;
}
/**
* Ensure an executable GraphQL schema is present.
*
* @return \GraphQL\Type\Schema
*/
public function prepSchema(): Schema
{
if (empty($this->executableSchema)) {
$this->executableSchema = $this->schemaBuilder->build(
$this->documentAST()
);
}
return $this->executableSchema;
}
/**
* Construct the validation rules with values given in the config.
*
* @return \GraphQL\Validator\Rules\ValidationRule[]
*/
protected function getValidationRules(): array
{
return [
QueryComplexity::class => new QueryComplexity(config('lighthouse.security.max_query_complexity', 0)),
QueryDepth::class => new QueryDepth(config('lighthouse.security.max_query_depth', 0)),
DisableIntrospection::class => new DisableIntrospection(config('lighthouse.security.disable_introspection', false)),
];
}
/**
* Get instance of DocumentAST.
*
* @return \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
public function documentAST(): DocumentAST
{
if (empty($this->documentAST)) {
$this->documentAST = config('lighthouse.cache.enable')
? app('cache')
->remember(
config('lighthouse.cache.key'),
config('lighthouse.cache.ttl'),
function (): DocumentAST {
return $this->astBuilder->build();
}
)
: $this->astBuilder->build();
}
return $this->documentAST;
}
}
@@ -0,0 +1,181 @@
<?php
namespace Nuwave\Lighthouse;
use Closure;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Routing\Router;
use Illuminate\Validation\Validator;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Schema\NodeRegistry;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Console\QueryCommand;
use Nuwave\Lighthouse\Console\UnionCommand;
use Nuwave\Lighthouse\Console\ScalarCommand;
use Illuminate\Contracts\Container\Container;
use Nuwave\Lighthouse\Console\MutationCommand;
use Nuwave\Lighthouse\Schema\ResolverProvider;
use Nuwave\Lighthouse\Console\IdeHelperCommand;
use Nuwave\Lighthouse\Console\InterfaceCommand;
use Nuwave\Lighthouse\Execution\ContextFactory;
use Nuwave\Lighthouse\Execution\GraphQLRequest;
use Nuwave\Lighthouse\Execution\SingleResponse;
use Nuwave\Lighthouse\Execution\Utils\GlobalId;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Console\ClearCacheCommand;
use Nuwave\Lighthouse\Console\PrintSchemaCommand;
use Nuwave\Lighthouse\Execution\GraphQLValidator;
use Laravel\Lumen\Application as LumenApplication;
use Nuwave\Lighthouse\Console\SubscriptionCommand;
use Nuwave\Lighthouse\Execution\LighthouseRequest;
use Nuwave\Lighthouse\Schema\Source\SchemaStitcher;
use Nuwave\Lighthouse\Console\ValidateSchemaCommand;
use Nuwave\Lighthouse\Execution\MultipartFormRequest;
use Illuminate\Validation\Factory as ValidationFactory;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver;
use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse;
use Illuminate\Foundation\Application as LaravelApplication;
use Nuwave\Lighthouse\Support\Http\Responses\ResponseStream;
use Nuwave\Lighthouse\Support\Compatibility\MiddlewareAdapter;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Nuwave\Lighthouse\Support\Compatibility\LumenMiddlewareAdapter;
use Nuwave\Lighthouse\Support\Compatibility\LaravelMiddlewareAdapter;
use Nuwave\Lighthouse\Support\Contracts\GlobalId as GlobalIdContract;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;
class LighthouseServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @param \Illuminate\Validation\Factory $validationFactory
* @param \Illuminate\Contracts\Config\Repository $configRepository
* @return void
*/
public function boot(ValidationFactory $validationFactory, ConfigRepository $configRepository): void
{
$this->publishes([
__DIR__.'/../config/config.php' => $this->app->make('path.config').DIRECTORY_SEPARATOR.'lighthouse.php',
], 'config');
$this->publishes([
__DIR__.'/../assets/default-schema.graphql' => $configRepository->get('lighthouse.schema.register'),
], 'schema');
$this->loadRoutesFrom(__DIR__.'/Support/Http/routes.php');
$validationFactory->resolver(
function ($translator, array $data, array $rules, array $messages, array $customAttributes): Validator {
// This determines whether we are resolving a GraphQL field
return Arr::has($customAttributes, ['root', 'context', 'resolveInfo'])
? new GraphQLValidator($translator, $data, $rules, $messages, $customAttributes)
: new Validator($translator, $data, $rules, $messages, $customAttributes);
}
);
}
/**
* Load routes from provided path.
*
* @param string $path
* @return void
*/
protected function loadRoutesFrom($path): void
{
if (Str::contains($this->app->version(), 'Lumen')) {
require realpath($path);
return;
}
parent::loadRoutesFrom($path);
}
/**
* Register any application services.
*
* @return void
*/
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/config.php', 'lighthouse');
$this->app->singleton(GraphQL::class);
$this->app->singleton(DirectiveFactory::class);
$this->app->singleton(NodeRegistry::class);
$this->app->singleton(TypeRegistry::class);
$this->app->singleton(CreatesContext::class, ContextFactory::class);
$this->app->singleton(CanStreamResponse::class, ResponseStream::class);
$this->app->bind(CreatesResponse::class, SingleResponse::class);
$this->app->bind(GlobalIdContract::class, GlobalId::class);
$this->app->singleton(GraphQLRequest::class, function (Container $app): GraphQLRequest {
/** @var \Illuminate\Http\Request $request */
$request = $app->make('request');
return Str::startsWith(
$request->header('Content-Type'),
'multipart/form-data'
)
? new MultipartFormRequest($request)
: new LighthouseRequest($request);
});
$this->app->singleton(SchemaSourceProvider::class, function (): SchemaStitcher {
return new SchemaStitcher(
config('lighthouse.schema.register', '')
);
});
$this->app->bind(ProvidesResolver::class, ResolverProvider::class);
$this->app->bind(ProvidesSubscriptionResolver::class, function (): ProvidesSubscriptionResolver {
return new class implements ProvidesSubscriptionResolver {
public function provideSubscriptionResolver(FieldValue $fieldValue): Closure
{
throw new Exception(
'Add the SubscriptionServiceProvider to your config/app.php to enable subscriptions.'
);
}
};
});
$this->app->singleton(MiddlewareAdapter::class, function (Container $app): MiddlewareAdapter {
// prefer using fully-qualified class names here when referring to Laravel-only or Lumen-only classes
if ($app instanceof LaravelApplication) {
return new LaravelMiddlewareAdapter(
$app->get(Router::class)
);
} elseif ($app instanceof LumenApplication) {
return new LumenMiddlewareAdapter($app);
}
throw new Exception(
'Could not correctly determine Laravel framework flavor, got '.get_class($app).'.'
);
});
if ($this->app->runningInConsole()) {
$this->commands([
ClearCacheCommand::class,
IdeHelperCommand::class,
InterfaceCommand::class,
MutationCommand::class,
PrintSchemaCommand::class,
QueryCommand::class,
ScalarCommand::class,
SubscriptionCommand::class,
UnionCommand::class,
ValidateSchemaCommand::class,
]);
}
}
}
@@ -0,0 +1,80 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ConnectionField
{
/**
* Resolve page info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator
* @return array
*/
public function pageInfoResolver(LengthAwarePaginator $paginator): array
{
return [
'total' => $paginator->total(),
'count' => $paginator->count(),
'currentPage' => $paginator->currentPage(),
'lastPage' => $paginator->lastPage(),
'hasNextPage' => $paginator->hasMorePages(),
'hasPreviousPage' => $paginator->currentPage() > 1,
'startCursor' => $paginator->firstItem()
? Cursor::encode($paginator->firstItem())
: null,
'endCursor' => $paginator->lastItem()
? Cursor::encode($paginator->lastItem())
: null,
];
}
/**
* Resolve edges for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator
* @param array $args
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return \Illuminate\Support\Collection
*/
public function edgeResolver(LengthAwarePaginator $paginator, $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection
{
$returnTypeFields = $resolveInfo
->returnType
->ofType
->getFields();
$firstItem = $paginator->firstItem();
return $paginator
->values()
->map(function ($item, $index) use ($firstItem, $returnTypeFields): array {
$data = [];
foreach ($returnTypeFields as $field) {
switch ($field->name) {
case 'cursor':
$data['cursor'] = Cursor::encode($firstItem + $index);
break;
case 'node':
$data['node'] = $item;
break;
default:
// All other fields on the return type are assumed to be part
// of the edge, so we try to locate them in the pivot attribute
if (isset($item->pivot->{$field->name})) {
$data[$field->name] = $item->pivot->{$field->name};
}
}
}
return $data;
});
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Arr;
/**
* Encode and decode pagination cursors.
*
* Currently, the underlying pagination Query uses offset based navigation, so
* this basically just encodes an offset. This is enough to satisfy the constraints
* that Relay has, but not a clean permanent solution.
*
* TODO Implement actual cursor pagination https://github.com/nuwave/lighthouse/issues/311
*/
class Cursor
{
/**
* Decode cursor from query arguments.
*
* If no 'after' argument is provided or the contents are not a valid base64 string,
* this will return 0. That will effectively reset pagination, so the user gets the
* first slice.
*
* @param array $args
* @return int
*/
public static function decode(array $args): int
{
if ($cursor = Arr::get($args, 'after')) {
return (int) base64_decode($cursor);
}
return 0;
}
/**
* Encode the given offset to make the implementation opaque.
*
* @param int $offset
* @return string
*/
public static function encode(int $offset): string
{
return base64_encode($offset);
}
}
@@ -0,0 +1,174 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
class PaginationManipulator
{
/**
* Transform the definition for a field to a field with pagination.
*
* This makes either an offset-based Paginator or a cursor-based Connection.
* The types in between are automatically generated and applied to the schema.
*
* @param \Nuwave\Lighthouse\Pagination\PaginationType $paginationType
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|null $edgeType
* @return void
*/
public static function transformToPaginatedField(
PaginationType $paginationType,
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null,
?ObjectTypeDefinitionNode $edgeType = null
): void {
if ($paginationType->isConnection()) {
self::registerConnection($fieldDefinition, $parentType, $documentAST, $defaultCount, $maxCount, $edgeType);
} else {
self::registerPaginator($fieldDefinition, $parentType, $documentAST, $defaultCount, $maxCount);
}
}
/**
* Register connection w/ schema.
*
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|null $edgeType
* @return void
* @throws DefinitionException
*/
public static function registerConnection(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null,
?ObjectTypeDefinitionNode $edgeType = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
if ($edgeType) {
$connectionEdgeName = $edgeType->name->value;
$connectionTypeName = "{$connectionEdgeName}Connection";
} else {
$connectionEdgeName = "{$fieldTypeName}Edge";
$connectionTypeName = "{$fieldTypeName}Connection";
}
$connectionFieldName = addslashes(ConnectionField::class);
$connectionType = PartialParser::objectTypeDefinition("
type $connectionTypeName {
pageInfo: PageInfo! @field(resolver: \"{$connectionFieldName}@pageInfoResolver\")
edges: [$connectionEdgeName] @field(resolver: \"{$connectionFieldName}@edgeResolver\")
}
");
$connectionEdge = $edgeType
?? $documentAST->types[$connectionEdgeName]
?? PartialParser::objectTypeDefinition("
type $connectionEdgeName {
node: $fieldTypeName
cursor: String!
}
");
$inputValueDefinitions = [
self::countArgument('first', $defaultCount, $maxCount),
"\"A cursor after which elements are returned.\"\nafter: String",
];
$connectionArguments = PartialParser::inputValueDefinitions($inputValueDefinitions);
$fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $connectionArguments);
$fieldDefinition->type = PartialParser::namedType($connectionTypeName);
$parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]);
$documentAST->setTypeDefinition($connectionType);
$documentAST->setTypeDefinition($connectionEdge);
}
/**
* Register paginator w/ schema.
*
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @return void
*/
public static function registerPaginator(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
$paginatorTypeName = "{$fieldTypeName}Paginator";
$paginatorFieldClassName = addslashes(PaginatorField::class);
$paginatorType = PartialParser::objectTypeDefinition("
type $paginatorTypeName {
paginatorInfo: PaginatorInfo! @field(resolver: \"{$paginatorFieldClassName}@paginatorInfoResolver\")
data: [$fieldTypeName!]! @field(resolver: \"{$paginatorFieldClassName}@dataResolver\")
}
");
$inputValueDefinitions = [
self::countArgument(config('lighthouse.pagination_amount_argument'), $defaultCount, $maxCount),
"\"The offset from which elements are returned.\"\npage: Int",
];
$paginationArguments = PartialParser::inputValueDefinitions($inputValueDefinitions);
$fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $paginationArguments);
$fieldDefinition->type = PartialParser::namedType($paginatorTypeName);
$parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]);
$documentAST->setTypeDefinition($paginatorType);
}
/**
* Build the count argument definition string, considering default and max values.
*
* @param string $argumentName
* @param int|null $defaultCount
* @param int|null $maxCount
* @return string
*/
protected static function countArgument(string $argumentName, ?int $defaultCount = null, ?int $maxCount = null): string
{
$description = '"Limits number of fetched elements.';
if ($maxCount) {
$description .= ' Maximum allowed value: '.$maxCount.'.';
}
$description .= "\"\n";
$definition = $argumentName.': Int'
.($defaultCount
? ' = '.$defaultCount
: '!'
);
return $description.$definition;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
/**
* An enum-like class that contains the supported types of pagination.
*/
class PaginationType
{
const TYPE_PAGINATOR = 'paginator';
const PAGINATION_TYPE_CONNECTION = 'connection';
/**
* @var string PAGINATION_TYPE_PAGINATOR|PAGINATION_TYPE_CONNECTION
*/
protected $type;
/**
* PaginationType constructor.
*
* @param string $paginationType
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
public function __construct(string $paginationType)
{
switch ($paginationType) {
case 'default':
case 'paginator':
$this->type = self::TYPE_PAGINATOR;
break;
case 'connection':
case 'relay':
$this->type = self::PAGINATION_TYPE_CONNECTION;
break;
default:
throw new DefinitionException(
"Found invalid pagination type: {$paginationType}"
);
}
}
public function isPaginator(): bool
{
return $this->type === self::TYPE_PAGINATOR;
}
public function isConnection(): bool
{
return $this->type === self::PAGINATION_TYPE_CONNECTION;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use GraphQL\Error\Error;
use Illuminate\Support\Arr;
class PaginationUtils
{
/**
* Calculate the current page to inform the user about the pagination state.
*
* @param int $first
* @param int $after
* @param int $defaultPage
* @return int
*/
public static function calculateCurrentPage(int $first, int $after, int $defaultPage = 1): int
{
return $first && $after
? (int) floor(($first + $after) / $first)
: $defaultPage;
}
/**
* @param mixed[] $args
* @param \Nuwave\Lighthouse\Pagination\PaginationType|null $paginationType
* @param int|null $paginateMaxCount
* @return int[] A pair consisting of first and page
*
* @throws \GraphQL\Error\Error
*/
public static function extractArgs(array $args, ?PaginationType $paginationType, ?int $paginateMaxCount): array
{
if ($paginationType->isConnection()) {
/** @var int $first */
$first = $args['first'];
$page = self::calculateCurrentPage(
$first,
Cursor::decode($args)
);
} else {
/** @var int $first */
$first = $args[config('lighthouse.pagination_amount_argument')];
$page = Arr::get($args, 'page', 1);
}
if ($first <= 0) {
throw new Error(
"Requested pagination amount must be more than 0, got $first"
);
}
// Make sure the maximum pagination count is not exceeded
if (
$paginateMaxCount !== null
&& $first > $paginateMaxCount
) {
throw new Error(
"Maximum number of {$paginateMaxCount} requested items exceeded. Fetch smaller chunks."
);
}
return [$first, $page];
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class PaginatorField
{
/**
* Resolve paginator info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return array
*/
public function paginatorInfoResolver(LengthAwarePaginator $root): array
{
return [
'count' => $root->count(),
'currentPage' => $root->currentPage(),
'firstItem' => $root->firstItem(),
'hasMorePages' => $root->hasMorePages(),
'lastItem' => $root->lastItem(),
'lastPage' => $root->lastPage(),
'perPage' => $root->perPage(),
'total' => $root->total(),
];
}
/**
* Resolve data for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return \Illuminate\Support\Collection
*/
public function dataResolver(LengthAwarePaginator $root): Collection
{
return $root->values();
}
}
+364
View File
@@ -0,0 +1,364 @@
<?php
namespace Nuwave\Lighthouse\Schema\AST;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Events\BuildSchemaString;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
use Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator;
class ASTBuilder
{
/**
* The directive factory.
*
* @var \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory
*/
protected $directiveFactory;
/**
* The event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $eventDispatcher;
/**
* The schema source provider.
*
* @var \Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider
*/
protected $schemaSourceProvider;
/**
* The document AST.
*
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
protected $documentAST;
/**
* ASTBuilder constructor.
*
* @param \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory $directiveFactory
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
* @param \Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider $schemaSourceProvider
* @return void
*/
public function __construct(
DirectiveFactory $directiveFactory,
EventDispatcher $eventDispatcher,
SchemaSourceProvider $schemaSourceProvider
) {
$this->directiveFactory = $directiveFactory;
$this->eventDispatcher = $eventDispatcher;
$this->schemaSourceProvider = $schemaSourceProvider;
}
/**
* Get the schema string and build an AST out of it.
*
* @return \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
public function build(): DocumentAST
{
$schemaString = $this->schemaSourceProvider->getSchemaString();
// Allow to register listeners that add in additional schema definitions.
// This can be used by plugins to hook into the schema building process
// while still allowing the user to add in their schema as usual.
$additionalSchemas = (array) $this->eventDispatcher->dispatch(
new BuildSchemaString($schemaString)
);
$this->documentAST = DocumentAST::fromSource(
implode(
PHP_EOL,
Arr::prepend($additionalSchemas, $schemaString)
)
);
// Apply transformations from directives
$this->applyTypeDefinitionManipulators();
$this->applyTypeExtensionManipulators();
$this->applyFieldManipulators();
$this->applyArgManipulators();
$this->addPaginationInfoTypes();
$this->addNodeSupport();
$this->addOrderByTypes();
// Listeners may manipulate the DocumentAST that is passed by reference
// into the ManipulateAST event. This can be useful for extensions
// that want to programmatically change the schema.
$this->eventDispatcher->dispatch(
new ManipulateAST($this->documentAST)
);
return $this->documentAST;
}
/**
* Apply directives on type definitions that can manipulate the AST.
*
* @return void
*/
protected function applyTypeDefinitionManipulators(): void
{
foreach ($this->documentAST->types as $typeDefinition) {
/** @var \Nuwave\Lighthouse\Support\Contracts\TypeManipulator $typeDefinitionManipulator */
foreach (
$this->directiveFactory->createAssociatedDirectivesOfType($typeDefinition, TypeManipulator::class)
as $typeDefinitionManipulator
) {
$typeDefinitionManipulator->manipulateTypeDefinition($this->documentAST, $typeDefinition);
}
}
}
/**
* Apply directives on type extensions that can manipulate the AST.
*
* @return void
*/
protected function applyTypeExtensionManipulators(): void
{
foreach ($this->documentAST->typeExtensions as $typeName => $typeExtensionsList) {
/** @var \GraphQL\Language\AST\TypeExtensionNode $typeExtension */
foreach ($typeExtensionsList as $typeExtension) {
/** @var \Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator $typeExtensionManipulator */
foreach (
$this->directiveFactory->createAssociatedDirectivesOfType($typeExtension, TypeExtensionManipulator::class)
as $typeExtensionManipulator
) {
$typeExtensionManipulator->manipulatetypeExtension($this->documentAST, $typeExtension);
}
// After manipulation on the type extension has been done,
// we can merge its fields with the original type
if ($typeExtension instanceof ObjectTypeExtensionNode) {
$relatedObjectType = $this->documentAST->types[$typeName];
$relatedObjectType->fields = ASTHelper::mergeUniqueNodeList(
$relatedObjectType->fields,
$typeExtension->fields
);
}
}
}
}
/**
* Apply directives on fields that can manipulate the AST.
*
* @return void
*/
protected function applyFieldManipulators(): void
{
foreach ($this->documentAST->types as $typeDefinition) {
if ($typeDefinition instanceof ObjectTypeDefinitionNode) {
foreach ($typeDefinition->fields as $fieldDefinition) {
/** @var \Nuwave\Lighthouse\Support\Contracts\FieldManipulator $fieldManipulator */
foreach (
$this->directiveFactory->createAssociatedDirectivesOfType($fieldDefinition, FieldManipulator::class)
as $fieldManipulator
) {
$fieldManipulator->manipulateFieldDefinition($this->documentAST, $fieldDefinition, $typeDefinition);
}
}
}
}
}
/**
* Apply directives on args that can manipulate the AST.
*
* @return void
*/
protected function applyArgManipulators(): void
{
foreach ($this->documentAST->types as $typeDefinition) {
if ($typeDefinition instanceof ObjectTypeDefinitionNode) {
foreach ($typeDefinition->fields as $fieldDefinition) {
foreach ($fieldDefinition->arguments as $argumentDefinition) {
/** @var \Nuwave\Lighthouse\Support\Contracts\ArgManipulator $argManipulator */
foreach (
$this->directiveFactory->createAssociatedDirectivesOfType($argumentDefinition, ArgManipulator::class)
as $argManipulator
) {
$argManipulator->manipulateArgDefinition(
$this->documentAST,
$argumentDefinition,
$fieldDefinition,
$typeDefinition
);
}
}
}
}
}
}
/**
* Add the types required for pagination.
*
* @return void
*/
protected function addPaginationInfoTypes(): void
{
$this->documentAST->setTypeDefinition(
PartialParser::objectTypeDefinition('
type PaginatorInfo {
"Total count of available items in the page."
count: Int!
"Current pagination page."
currentPage: Int!
"Index of first item in the current page."
firstItem: Int
"If collection has more pages."
hasMorePages: Boolean!
"Index of last item in the current page."
lastItem: Int
"Last page number of the collection."
lastPage: Int!
"Number of items per page in the collection."
perPage: Int!
"Total items available in the collection."
total: Int!
}
')
);
$this->documentAST->setTypeDefinition(
PartialParser::objectTypeDefinition('
type PageInfo {
"When paginating forwards, are there more items?"
hasNextPage: Boolean!
"When paginating backwards, are there more items?"
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String
"Total number of node in connection."
total: Int
"Count of nodes in current request."
count: Int
"Current page of request."
currentPage: Int
"Last page in connection."
lastPage: Int
}
')
);
}
/**
* Returns whether or not the given interface is used within the defined types.
*
* @param string $interfaceName
*
* @return bool
*/
protected function hasTypeImplementingInterface(string $interfaceName): bool
{
foreach ($this->documentAST->types as $typeDefinition) {
if ($typeDefinition instanceof ObjectTypeDefinitionNode) {
if (ASTHelper::typeImplementsInterface($typeDefinition, $interfaceName)) {
return true;
}
}
}
return false;
}
/**
* Inject the Node interface and a node field into the Query type.
*
* @return void
*/
protected function addNodeSupport(): void
{
// Only add the node type and node field if a type actually implements them
// Otherwise, a validation error is thrown
if (! $this->hasTypeImplementingInterface('Node')) {
return;
}
$globalId = config('lighthouse.global_id_field');
// Double slashes to escape the slashes in the namespace.
$this->documentAST->setTypeDefinition(
PartialParser::interfaceTypeDefinition(<<<GRAPHQL
"Node global interface"
interface Node @interface(resolveType: "Nuwave\\\Lighthouse\\\Schema\\\NodeRegistry@resolveType") {
"Global identifier that can be used to resolve any Node implementation."
$globalId: ID!
}
GRAPHQL
)
);
/** @var ObjectTypeDefinitionNode $queryType */
$queryType = $this->documentAST->types['Query'];
$queryType->fields = ASTHelper::mergeNodeList(
$queryType->fields,
[
PartialParser::fieldDefinition('
node(id: ID! @globalId): Node @field(resolver: "Nuwave\\\Lighthouse\\\Schema\\\NodeRegistry@resolve")
'),
]
);
}
/**
* Add types that are used for the @orderBy directive.
*
* @see \Nuwave\Lighthouse\Schema\Directives\OrderByDirective
*
* @return void
*/
protected function addOrderByTypes(): void
{
$this->documentAST->setTypeDefinition(
PartialParser::enumTypeDefinition('
enum SortOrder {
ASC
DESC
}
'
)
);
$this->documentAST->setTypeDefinition(
PartialParser::inputObjectTypeDefinition('
input OrderByClause {
field: String!
order: SortOrder!
}
')
);
}
}
+294
View File
@@ -0,0 +1,294 @@
<?php
namespace Nuwave\Lighthouse\Schema\AST;
use GraphQL\Utils\AST;
use GraphQL\Language\Parser;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\Type;
use GraphQL\Language\AST\NodeList;
use Illuminate\Support\Collection;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Directives\NamespaceDirective;
class ASTHelper
{
/**
* This function exists as a workaround for an issue within webonyx/graphql-php.
*
* The problem is that lists of definitions are usually NodeList objects - except
* when the list is empty, then it is []. This function corrects that inconsistency
* and allows the rest of our code to not worry about it until it is fixed.
*
* This issue is brought up here https://github.com/webonyx/graphql-php/issues/285
* Remove this method (and possibly the entire class) once it is resolved.
*
* @param \GraphQL\Language\AST\NodeList|array $original
* @param \GraphQL\Language\AST\NodeList|array $addition
* @return \GraphQL\Language\AST\NodeList
*/
public static function mergeNodeList($original, $addition): NodeList
{
if (! $original instanceof NodeList) {
$original = new NodeList($original);
}
return $original->merge($addition);
}
/**
* This function will merge two lists uniquely by name.
*
* @param \GraphQL\Language\AST\NodeList|array $original
* @param \GraphQL\Language\AST\NodeList|array $addition
* @param bool $overwriteDuplicates By default this throws if a collision occurs. If
* this is set to true, the fields of the original list will be overwritten.
* @return \GraphQL\Language\AST\NodeList
*/
public static function mergeUniqueNodeList($original, $addition, bool $overwriteDuplicates = false): NodeList
{
$newNames = (new Collection($addition))
->pluck('name.value')
->filter()
->all();
$remainingDefinitions = (new Collection($original))
->reject(function ($definition) use ($newNames, $overwriteDuplicates): bool {
$oldName = $definition->name->value;
$collisionOccurred = in_array(
$oldName,
$newNames
);
if ($collisionOccurred && ! $overwriteDuplicates) {
throw new DefinitionException(
"Duplicate definition {$oldName} found when merging."
);
}
return $collisionOccurred;
})
->values()
->all();
return self::mergeNodeList($remainingDefinitions, $addition);
}
/**
* @param \GraphQL\Language\AST\Node $definition
* @return string
*/
public static function getUnderlyingTypeName(Node $definition): string
{
$type = $definition->type;
if ($type instanceof ListTypeNode || $type instanceof NonNullTypeNode) {
$type = self::getUnderlyingNamedTypeNode($type);
}
return $type->name->value;
}
/**
* @param \GraphQL\Language\AST\Node $node
* @return \GraphQL\Language\AST\NamedTypeNode
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
public static function getUnderlyingNamedTypeNode(Node $node): NamedTypeNode
{
if ($node instanceof NamedTypeNode) {
return $node;
}
$type = data_get($node, 'type');
if (! $type) {
throw new DefinitionException(
"The node '$node->kind' does not have a type associated with it."
);
}
return self::getUnderlyingNamedTypeNode($type);
}
/**
* Does the given directive have an argument of the given name?
*
* @param \GraphQL\Language\AST\DirectiveNode $directiveDefinition
* @param string $name
* @return bool
*/
public static function directiveHasArgument(DirectiveNode $directiveDefinition, string $name): bool
{
return (new Collection($directiveDefinition->arguments))
->contains(function (ArgumentNode $argumentNode) use ($name): bool {
return $argumentNode->name->value === $name;
});
}
/**
* @param \GraphQL\Language\AST\DirectiveNode $directive
* @param string $name
* @param mixed|null $default
* @return mixed|null
*/
public static function directiveArgValue(DirectiveNode $directive, string $name, $default = null)
{
$arg = (new Collection($directive->arguments))
->first(function (ArgumentNode $argumentNode) use ($name): bool {
return $argumentNode->name->value === $name;
});
return $arg
? self::argValue($arg, $default)
: $default;
}
/**
* Get argument's value.
*
* @param \GraphQL\Language\AST\ArgumentNode $arg
* @param mixed $default
* @return mixed
*/
public static function argValue(ArgumentNode $arg, $default = null)
{
$valueNode = $arg->value;
if (! $valueNode) {
return $default;
}
return AST::valueFromASTUntyped($valueNode);
}
/**
* Return the PHP internal value of an arguments default value.
*
* @param \GraphQL\Language\AST\ValueNode $defaultValue
* @param \GraphQL\Type\Definition\Type $argumentType
* @return mixed
*/
public static function defaultValueForArgument(ValueNode $defaultValue, Type $argumentType)
{
// webonyx/graphql-php expects the internal value here, whereas the
// SDL uses the ENUM's name, so we run the conversion here
if ($argumentType instanceof EnumType) {
return $argumentType
->getValue(
$defaultValue->value
)
->value;
}
return AST::valueFromAST($defaultValue, $argumentType);
}
/**
* This can be at most one directive, since directives can only be used once per location.
*
* @param \GraphQL\Language\AST\Node $definitionNode
* @param string $name
* @return \GraphQL\Language\AST\DirectiveNode|null
*/
public static function directiveDefinition(Node $definitionNode, string $name): ?DirectiveNode
{
return (new Collection($definitionNode->directives))
->first(function (DirectiveNode $directiveDefinitionNode) use ($name): bool {
return $directiveDefinitionNode->name->value === $name;
});
}
/**
* Directives might have an additional namespace associated with them, set via the "@namespace" directive.
*
* @param \GraphQL\Language\AST\Node $definitionNode
* @param string $directiveName
* @return string
*/
public static function getNamespaceForDirective(Node $definitionNode, string $directiveName): string
{
$namespaceDirective = static::directiveDefinition(
$definitionNode,
NamespaceDirective::NAME
);
return $namespaceDirective
// The namespace directive can contain an argument with the name of the
// current directive, in which case it applies here
? static::directiveArgValue($namespaceDirective, $directiveName, '')
// Default to an empty namespace if the namespace directive does not exist
: '';
}
/**
* Attach directive to all registered object type fields.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\DirectiveNode $directive
* @return void
*/
public static function attachDirectiveToObjectTypeFields(DocumentAST $documentAST, DirectiveNode $directive): void
{
foreach ($documentAST->types as $typeDefinition) {
if ($typeDefinition instanceof ObjectTypeDefinitionNode) {
foreach ($typeDefinition->fields as $fieldDefinition) {
$fieldDefinition->directives = $fieldDefinition->directives->merge([$directive]);
}
}
}
}
/**
* Add the "Node" interface and a global ID field to an object type.
*
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $objectType
* @return \GraphQL\Language\AST\ObjectTypeDefinitionNode
*/
public static function attachNodeInterfaceToObjectType(ObjectTypeDefinitionNode $objectType): ObjectTypeDefinitionNode
{
$objectType->interfaces = self::mergeNodeList(
$objectType->interfaces,
[
Parser::parseType(
'Node',
['noLocation' => true]
),
]
);
$globalIdFieldDefinition = PartialParser::fieldDefinition(
config('lighthouse.global_id_field').': ID! @globalId'
);
$objectType->fields = $objectType->fields->merge([$globalIdFieldDefinition]);
return $objectType;
}
/**
* Checks the given type to see whether it implements the given interface.
*
* @param ObjectTypeDefinitionNode $type
* @param string $interfaceName
*
* @return bool
*/
public static function typeImplementsInterface(ObjectTypeDefinitionNode $type, string $interfaceName): bool
{
foreach ($type->interfaces as $interface) {
if ($interface->name->value === $interfaceName) {
return true;
}
}
return false;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace Nuwave\Lighthouse\Schema\AST;
use Exception;
use Serializable;
use GraphQL\Language\Parser;
use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\ParseException;
use GraphQL\Language\AST\DirectiveDefinitionNode;
class DocumentAST implements Serializable
{
/**
* The types within the schema.
*
* ['foo' => FooType].
*
* @var NodeList<TypeDefinitionNode>
*/
public $types = [];
/**
* The type extensions within the parsed document.
*
* Will NOT be kept after unserialization, as the type
* extensions are merged with the types before.
*
* ['foo' => [0 => FooExtension, 1 => FooExtension]].
*
* @var NodeList<TypeExtensionNode>[]
*/
public $typeExtensions = [];
/**
* Client directive definitions.
*
* ['foo' => FooDirective].
*
* @var NodeList<DirectiveDefinitionNode>
*/
public $directives = [];
/**
* Create a new DocumentAST instance from a schema.
*
* @param string $schema
* @return static
*
* @throws \Nuwave\Lighthouse\Exceptions\ParseException
*/
public static function fromSource(string $schema): self
{
try {
$documentNode = Parser::parse(
$schema,
// Ignore location since it only bloats the AST
['noLocation' => true]
);
} catch (SyntaxError $syntaxError) {
// Throw our own error class instead, since otherwise a schema definition
// error would get rendered to the Client.
throw new ParseException(
$syntaxError->getMessage()
);
}
$instance = new self;
foreach ($documentNode->definitions as $definition) {
if ($definition instanceof TypeDefinitionNode) {
// Store the types in an associative array for quick lookup
$instance->types[$definition->name->value] = $definition;
} elseif ($definition instanceof TypeExtensionNode) {
// Multiple type extensions for the same name can exist
$instance->typeExtensions[$definition->name->value] [] = $definition;
} elseif ($definition instanceof DirectiveDefinitionNode) {
$instance->directives[$definition->name->value] = $definition;
} else {
throw new Exception(
'Unknown definition type'
);
}
}
return $instance;
}
/**
* Serialize the final AST.
*
* We exclude the type extensions stored in $typeExtensions,
* as they are merged with the actual types at this point.
*
* @return string
*/
public function serialize(): string
{
$nodeToArray = function (Node $node): array {
return $node->toArray(true);
};
return serialize([
'types' => array_map($nodeToArray, $this->types),
'directives' => array_map($nodeToArray, $this->directives),
]);
}
/**
* Unserialize the AST.
*
* @param string $serialized
*/
public function unserialize($serialized): void
{
[
'types' => $types,
'directives' => $directives,
] = unserialize($serialized);
// Utilize the NodeList for lazy unserialization for performance gains.
// Until they are accessed by name, they are kept in their array form.
$this->types = new NodeList($types);
$this->directives = new NodeList($directives);
}
/**
* Set a type definition in the AST.
*
* This operation will overwrite existing definitions with the same name.
*
* @param \GraphQL\Language\AST\TypeDefinitionNode $type
* @return $this
*/
public function setTypeDefinition(TypeDefinitionNode $type): self
{
$this->types[$type->name->value] = $type;
return $this;
}
/**
* Set a directive definition in the AST.
*
* This operation will overwrite existing definitions with the same name.
*
* @param \GraphQL\Language\AST\DirectiveDefinitionNode $directive
* @return $this
*/
public function setDirectiveDefinition(DirectiveDefinitionNode $directive): self
{
$this->directives[$directive->name->value] = $directive;
return $this;
}
}
@@ -0,0 +1,323 @@
<?php
namespace Nuwave\Lighthouse\Schema\AST;
use GraphQL\Language\Parser;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\ParseException;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
class PartialParser
{
/**
* @param string[] $objectTypes
* @return \GraphQL\Language\AST\ObjectTypeDefinitionNode[]
*/
public static function objectTypeDefinitions(array $objectTypes): array
{
return array_map(function ($objectType): ObjectTypeDefinitionNode {
return self::objectTypeDefinition($objectType);
}, $objectTypes);
}
/**
* @param string $definition
* @return \GraphQL\Language\AST\ObjectTypeDefinitionNode
*/
public static function objectTypeDefinition(string $definition): ObjectTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($definition)->definitions,
ObjectTypeDefinitionNode::class
);
}
/**
* @param string $inputValueDefinition
* @return \GraphQL\Language\AST\InputValueDefinitionNode
*/
public static function inputValueDefinition(string $inputValueDefinition): InputValueDefinitionNode
{
return self::getFirstAndValidateType(
self::fieldDefinition("field($inputValueDefinition): String")->arguments,
InputValueDefinitionNode::class
);
}
/**
* @param string[] $inputValueDefinitions
* @return \GraphQL\Language\AST\InputValueDefinitionNode[]
*/
public static function inputValueDefinitions(array $inputValueDefinitions): array
{
return array_map(
function (string $inputValueDefinition): InputValueDefinitionNode {
return self::inputValueDefinition($inputValueDefinition);
},
$inputValueDefinitions
);
}
/**
* @param string $argumentDefinition
* @return \GraphQL\Language\AST\ArgumentNode
*/
public static function argument(string $argumentDefinition): ArgumentNode
{
return self::getFirstAndValidateType(
self::field("field($argumentDefinition)")->arguments,
ArgumentNode::class
);
}
/**
* @param string[] $argumentDefinitions
* @return \GraphQL\Language\AST\ArgumentNode[]
*/
public static function arguments(array $argumentDefinitions): array
{
return array_map(
function (string $argumentDefinition): ArgumentNode {
return self::argument($argumentDefinition);
},
$argumentDefinitions
);
}
/**
* @param string $field
* @return \GraphQL\Language\AST\FieldNode
*/
public static function field(string $field): FieldNode
{
return self::getFirstAndValidateType(
self::operationDefinition("{ $field }")->selectionSet->selections,
FieldNode::class
);
}
/**
* @param string $operation
* @return \GraphQL\Language\AST\OperationDefinitionNode
*/
public static function operationDefinition(string $operation): OperationDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($operation)->definitions,
OperationDefinitionNode::class
);
}
/**
* @param string $fieldDefinition
* @return \GraphQL\Language\AST\FieldDefinitionNode
*/
public static function fieldDefinition(string $fieldDefinition): FieldDefinitionNode
{
return self::getFirstAndValidateType(
self::objectTypeDefinition("type Dummy { $fieldDefinition }")->fields,
FieldDefinitionNode::class
);
}
/**
* @param string $directive
* @return \GraphQL\Language\AST\DirectiveNode
*/
public static function directive(string $directive): DirectiveNode
{
return self::getFirstAndValidateType(
self::objectTypeDefinition("
type Dummy $directive {
dummy: Int
}
")->directives,
DirectiveNode::class
);
}
/**
* @param string[] $directives
* @return \GraphQL\Language\AST\DirectiveNode[]
*/
public static function directives(array $directives): array
{
return array_map(
function (string $directive): DirectiveNode {
return self::directive($directive);
},
$directives
);
}
/**
* @param string $directiveDefinition
* @return \GraphQL\Language\AST\DirectiveDefinitionNode
*/
public static function directiveDefinition(string $directiveDefinition): DirectiveDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($directiveDefinition)->definitions,
DirectiveDefinitionNode::class
);
}
/**
* @param string[] $directiveDefinitions
* @return \GraphQL\Language\AST\DirectiveDefinitionNode[]
*/
public static function directiveDefinitions(array $directiveDefinitions): array
{
return array_map(
function (string $directiveDefinition): DirectiveDefinitionNode {
return self::directiveDefinition($directiveDefinition);
},
$directiveDefinitions
);
}
/**
* @param string $interfaceDefinition
* @return \GraphQL\Language\AST\InterfaceTypeDefinitionNode
*/
public static function interfaceTypeDefinition(string $interfaceDefinition): InterfaceTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($interfaceDefinition)->definitions,
InterfaceTypeDefinitionNode::class
);
}
/**
* @param string $unionDefinition
* @return \GraphQL\Language\AST\UnionTypeDefinitionNode
*/
public static function unionTypeDefinition(string $unionDefinition): UnionTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($unionDefinition)->definitions,
UnionTypeDefinitionNode::class
);
}
/**
* @param string $inputTypeDefinition
* @return \GraphQL\Language\AST\InputObjectTypeDefinitionNode
*/
public static function inputObjectTypeDefinition(string $inputTypeDefinition): InputObjectTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($inputTypeDefinition)->definitions,
InputObjectTypeDefinitionNode::class
);
}
/**
* @param string $scalarDefinition
* @return \GraphQL\Language\AST\ScalarTypeDefinitionNode
*/
public static function scalarTypeDefinition(string $scalarDefinition): ScalarTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($scalarDefinition)->definitions,
ScalarTypeDefinitionNode::class
);
}
/**
* @param string $enumDefinition
* @return \GraphQL\Language\AST\EnumTypeDefinitionNode
*/
public static function enumTypeDefinition(string $enumDefinition): EnumTypeDefinitionNode
{
return self::getFirstAndValidateType(
self::parse($enumDefinition)->definitions,
EnumTypeDefinitionNode::class
);
}
/**
* @param string $typeName
* @return \GraphQL\Language\AST\NamedTypeNode
*
* @throws \Nuwave\Lighthouse\Exceptions\ParseException
*/
public static function namedType(string $typeName): NamedTypeNode
{
return self::validateType(
self::parseType($typeName),
NamedTypeNode::class
);
}
/**
* @param string $definition
* @return \GraphQL\Language\AST\DocumentNode
*/
protected static function parse(string $definition): DocumentNode
{
// Ignore location since it only bloats the AST
return Parser::parse($definition, ['noLocation' => true]);
}
/**
* @param string $definition
* @return \GraphQL\Language\AST\Node
*/
protected static function parseType(string $definition): Node
{
// Ignore location since it only bloats the AST
return Parser::parseType($definition, ['noLocation' => true]);
}
/**
* Get the first Node from a given NodeList and validate it.
*
* @param \GraphQL\Language\AST\NodeList $list
* @param string $expectedType
* @return \GraphQL\Language\AST\Node
*
* @throws \Nuwave\Lighthouse\Exceptions\ParseException
*/
protected static function getFirstAndValidateType(NodeList $list, string $expectedType): Node
{
if ($list->count() !== 1) {
throw new ParseException('More than one definition was found in the passed in schema.');
}
$node = $list[0];
return self::validateType($node, $expectedType);
}
/**
* @param \GraphQL\Language\AST\Node $node
* @param string $expectedType
* @return \GraphQL\Language\AST\Node
*
* @throws \Nuwave\Lighthouse\Exceptions\ParseException
*/
protected static function validateType(Node $node, string $expectedType): Node
{
if (! $node instanceof $expectedType) {
throw new ParseException("The given definition was not of type: $expectedType");
}
return $node;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Nuwave\Lighthouse\Schema;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class Context implements GraphQLContext
{
/**
* An instance of the incoming HTTP request.
*
* @var \Illuminate\Http\Request
*/
public $request;
/**
* An instance of the currently authenticated user.
*
* @var \Illuminate\Contracts\Auth\Authenticatable|null
*/
public $user;
/**
* Create new context.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(Request $request)
{
$this->request = $request;
$this->user = $request->user();
}
/**
* Get instance of authenticated user.
*
* May be null since some fields may be accessible without authentication.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
return $this->user;
}
/**
* Get instance of request.
*
* @return \Illuminate\Http\Request
*/
public function request(): Request
{
return $this->request;
}
}
@@ -0,0 +1,100 @@
<?php
namespace Nuwave\Lighthouse\Schema\Conversion;
use GraphQL\Type\Definition\Type;
use GraphQL\Language\AST\NodeKind;
use Illuminate\Support\Collection;
use GraphQL\Language\AST\NamedTypeNode;
use Nuwave\Lighthouse\Schema\TypeRegistry;
class DefinitionNodeConverter
{
/**
* @var \Nuwave\Lighthouse\Schema\TypeRegistry
*/
protected $typeRegistry;
/**
* @param \Nuwave\Lighthouse\Schema\TypeRegistry $typeRegistry
* @return void
*/
public function __construct(TypeRegistry $typeRegistry)
{
$this->typeRegistry = $typeRegistry;
}
/**
* Convert a definition node to an executable Type.
*
* @param mixed $node
* @return \GraphQL\Type\Definition\Type
*/
public function toType($node): Type
{
return $this->convertWrappedDefinitionNode($node);
}
/**
* Unwrap the node if needed and convert to type.
*
* @param mixed $node
* @param string[] $wrappers
* @return \GraphQL\Type\Definition\Type
*/
protected function convertWrappedDefinitionNode($node, array $wrappers = []): Type
{
// Recursively unwrap the type and save the wrappers
if ($node->kind === NodeKind::NON_NULL_TYPE || $node->kind === NodeKind::LIST_TYPE) {
$wrappers[] = $node->kind;
return $this->convertWrappedDefinitionNode(
$node->type,
$wrappers
);
}
// Re-wrap the type by applying the wrappers in the reversed order
return (new Collection($wrappers))
->reverse()
->reduce(
function (Type $type, string $kind): Type {
if ($kind === NodeKind::NON_NULL_TYPE) {
return Type::nonNull($type);
}
if ($kind === NodeKind::LIST_TYPE) {
return Type::listOf($type);
}
return $type;
},
$this->convertNamedTypeNode($node)
);
}
/**
* Converted named node to type.
*
* @param \GraphQL\Language\AST\NamedTypeNode $node
* @return \GraphQL\Type\Definition\Type
*/
protected function convertNamedTypeNode(NamedTypeNode $node): Type
{
$nodeName = $node->name->value;
switch ($nodeName) {
case 'ID':
return Type::id();
case 'Int':
return Type::int();
case 'Boolean':
return Type::boolean();
case 'Float':
return Type::float();
case 'String':
return Type::string();
default:
return $this->typeRegistry->get($nodeName);
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Nuwave\Lighthouse\Schema;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Events\Dispatcher;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
class DirectiveNamespacer
{
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $dispatcher;
/**
* DirectiveNamespaces constructor.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function __construct(Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* A list of namespaces with directives in descending priority.
*
* @return string[]
*/
public function gather(): array
{
// When looking for a directive by name, the namespaces are tried in order
return (
new Collection([
// User defined directives (top priority)
config('lighthouse.namespaces.directives'),
// Plugin developers defined directives
$this->dispatcher->dispatch(new RegisterDirectiveNamespaces),
// Lighthouse defined directives
'Nuwave\\Lighthouse\\Schema\\Directives',
]))
->flatten()
->filter()
->all();
}
}
@@ -0,0 +1,68 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Collection;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class AllDirective extends BaseDirective implements DefinedDirective, FieldResolver
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'all';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @all(
"""
Specify the class name of the model to use.
This is only needed when the default model resolution does not work.
"""
model: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection {
/** @var \Illuminate\Database\Eloquent\Model $modelClass */
$modelClass = $this->getModelClass();
return $resolveInfo
->builder
->addScopes(
$this->directiveArgValue('scopes', [])
)
->apply(
$modelClass::query(),
$args
)
->get();
}
);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Illuminate\Contracts\Auth\Authenticatable;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class AuthDirective extends BaseDirective implements DefinedDirective, FieldResolver
{
/**
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $authFactory;
/**
* AuthDirective constructor.
*
* @param \Illuminate\Contracts\Auth\Factory $authFactory
* @return void
*/
public function __construct(AuthFactory $authFactory)
{
$this->authFactory = $authFactory;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'auth';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Return the currently authenticated user as the result of a query.
"""
directive @auth(
"""
Use a particular guard to retreive the user.
"""
guard: String
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
/** @var string|null $guard */
$guard = $this->directiveArgValue('guard');
return $fieldValue->setResolver(
function () use ($guard): ?Authenticatable {
return $this
->authFactory
->guard($guard)
->user();
}
);
}
}
@@ -0,0 +1,215 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use GraphQL\Language\AST\Node;
use Nuwave\Lighthouse\Support\Utils;
use GraphQL\Language\AST\DirectiveNode;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
abstract class BaseDirective implements Directive
{
/**
* The node the directive is defined on.
*
* @var \GraphQL\Language\AST\Node
*/
protected $definitionNode;
/**
* The hydrate function is called when retrieving a directive from the directive registry.
*
* @param \GraphQL\Language\AST\Node $definitionNode
* @return $this
*/
public function hydrate(Node $definitionNode): self
{
$this->definitionNode = $definitionNode;
return $this;
}
/**
* Get the directive definition associated with the current directive.
*
* @return \GraphQL\Language\AST\DirectiveNode
*/
protected function directiveDefinition(): DirectiveNode
{
return ASTHelper::directiveDefinition(
$this->definitionNode,
static::name()
);
}
/**
* Get directive argument value.
*
* @param string $name
* @param mixed|null $default
* @return mixed|null
*/
protected function directiveArgValue(string $name, $default = null)
{
return ASTHelper::directiveArgValue(
$this->directiveDefinition(),
$name,
$default
);
}
/**
* Does the current directive have an argument with the given name?
*
* @param string $name
* @return bool
*/
public function directiveHasArgument(string $name): bool
{
return ASTHelper::directiveHasArgument(
$this->directiveDefinition(),
$name
);
}
/**
* Get a Closure that is defined through an argument on the directive.
*
* @param string $argumentName
* @return \Closure
*/
public function getResolverFromArgument(string $argumentName): Closure
{
[$className, $methodName] = $this->getMethodArgumentParts($argumentName);
$namespacedClassName = $this->namespaceClassName($className);
return Utils::constructResolver($namespacedClassName, $methodName);
}
/**
* Get the model class from the `model` argument of the field.
*
* @param string $argumentName The default argument name "model" may be overwritten
* @return string
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
protected function getModelClass(string $argumentName = 'model'): string
{
$model = $this->directiveArgValue($argumentName);
// Fallback to using information from the schema definition as the model name
if (! $model) {
if ($this->definitionNode instanceof FieldDefinitionNode) {
$model = ASTHelper::getUnderlyingTypeName($this->definitionNode);
} elseif ($this->definitionNode instanceof ObjectTypeDefinitionNode) {
$model = $this->definitionNode->name->value;
}
}
if (! $model) {
throw new DirectiveException(
"A `model` argument must be assigned to the '{$this->name()}'directive on '{$this->definitionNode->name->value}"
);
}
return $this->namespaceModelClass($model);
}
/**
* @param string $classCandidate
* @param string[] $namespacesToTry
* @param callable $determineMatch
* @return string
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
protected function namespaceClassName(string $classCandidate, array $namespacesToTry = [], callable $determineMatch = null): string
{
// Always try the explicitly set namespace first
array_unshift(
$namespacesToTry,
ASTHelper::getNamespaceForDirective(
$this->definitionNode,
static::name()
)
);
if (! $determineMatch) {
$determineMatch = 'class_exists';
}
$className = Utils::namespaceClassname(
$classCandidate,
$namespacesToTry,
$determineMatch
);
if (! $className) {
throw new DirectiveException(
"No class '{$classCandidate}' was found for directive '{$this->name()}'"
);
}
return $className;
}
/**
* Split a single method argument into its parts.
*
* A method argument is expected to contain a class and a method name, separated by an @ symbol.
* e.g. "App\My\Class@methodName"
* This validates that exactly two parts are given and are not empty.
*
* @param string $argumentName
* @return string[] Contains two entries: [string $className, string $methodName]
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
protected function getMethodArgumentParts(string $argumentName): array
{
$argumentParts = explode(
'@',
$this->directiveArgValue($argumentName)
);
if (
count($argumentParts) > 2
|| empty($argumentParts[0])
) {
throw new DirectiveException(
"Directive '{$this->name()}' must have an argument '{$argumentName}' in the form 'ClassName@methodName' or 'ClassName'"
);
}
if (empty($argumentParts[1])) {
$argumentParts[1] = '__invoke';
}
return $argumentParts;
}
/**
* Try adding the default model namespace and ensure the given class is a model.
*
* @param string $modelClassCandidate
* @return string
*/
protected function namespaceModelClass(string $modelClassCandidate): string
{
return $this->namespaceClassName(
$modelClassCandidate,
(array) config('lighthouse.namespaces.models'),
function (string $classCandidate): bool {
return is_subclass_of($classCandidate, Model::class);
}
);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective;
class BcryptDirective implements ArgTransformerDirective, DefinedDirective
{
/**
* Directive name.
*
* @return string
*/
public function name(): string
{
return 'bcrypt';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Run the `bcrypt` function on the argument it is defined on.
"""
directive @bcrypt on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
/**
* Run Laravel's bcrypt helper on the argument.
*
* Useful for hashing passwords before inserting them into the database.
*
* @param string $argumentValue
* @return mixed
*/
public function transform($argumentValue): string
{
return bcrypt($argumentValue);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class BelongsToDirective extends RelationDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'belongsTo';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Resolves a field through the Eloquent `BelongsTo` relationship.
"""
directive @belongsTo(
"""
Specify the relationship method name in the model class,
if it is named different from the field in the schema.
"""
relation: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
) on FIELD_DEFINITION
SDL;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
class BelongsToManyDirective extends RelationDirective implements FieldResolver, FieldManipulator, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'belongsToMany';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Resolves a field through the Eloquent `BelongsToMany` relationship.
"""
directive @belongsToMany(
"""
Specify the relationship method name in the model class,
if it is named different from the field in the schema.
"""
relation: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
"""
ALlows to resolve the relation as a paginated list.
Allowed values: paginator, connection.
"""
type: String
"""
Specify the default quantity of elements to be returned.
Only applies when using pagination.
"""
defaultCount: Int
"""
Specify the maximum quantity of elements to be returned.
Only applies when using pagination.
"""
maxCount: Int
"""
Specify a custom type that implements the Edge interface
to extend edge object.
Only applies when using Relay style "connection" pagination.
"""
edgeType: String
) on FIELD_DEFINITION
SDL;
}
}
@@ -0,0 +1,72 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use GraphQL\Deferred;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Execution\Utils\Subscription;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class BroadcastDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'broadcast';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @broadcast(
"""
Name of the subscription that should be retriggered as a result of this operation..
"""
subscription: String!
"""
Specify whether or not the job should be queued.
This defaults to the global config option `lighthouse.subscriptions.queue_broadcasts`.
"""
shouldQueue: Boolean
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
// Ensure this is run after the other field middleware directives
$fieldValue = $next($fieldValue);
$resolver = $fieldValue->getResolver();
return $fieldValue->setResolver(function () use ($resolver) {
$resolved = call_user_func_array($resolver, func_get_args());
$subscriptionField = $this->directiveArgValue('subscription');
$shouldQueue = $this->directiveArgValue('shouldQueue');
if ($resolved instanceof Deferred) {
$resolved->then(function ($root) use ($subscriptionField, $shouldQueue): void {
Subscription::broadcast($subscriptionField, $root, $shouldQueue);
});
} else {
Subscription::broadcast($subscriptionField, $resolved, $shouldQueue);
}
return $resolved;
});
}
}
@@ -0,0 +1,53 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
class BuilderDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'builder';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Use an argument to modify the query builder for a field.
"""
directive @builder(
"""
Reference a method that is passed the query builder.
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
"""
method: String!
) on FIELD_DEFINITION
SDL;
}
/**
* Dynamically call a user-defined method to enhance the builder.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $value
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $value)
{
return call_user_func(
$this->getResolverFromArgument('method'),
$builder,
$value,
$this->definitionNode
);
}
}
@@ -0,0 +1,206 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Carbon\Carbon;
use GraphQL\Deferred;
use Illuminate\Cache\CacheManager;
use Illuminate\Support\Collection;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\Values\TypeValue;
use Nuwave\Lighthouse\Schema\Values\CacheValue;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class CacheDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/** @var \Illuminate\Cache\CacheManager */
protected $cacheManager;
/**
* @param \Illuminate\Cache\CacheManager $cacheManager
* @return void
*/
public function __construct(CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'cache';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Cache the result of a resolver.
"""
directive @cache(
"""
Set the duration it takes for the cache to expire in seconds.
If not given, the result will be stored forever.
"""
maxAge: Int
"""
Limit access to cached data to the currently authenticated user.
When the field is accessible by guest users, this will not have
any effect, they will access a shared cache.
"""
private: Boolean = false
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
$this->setCacheKeyOnParent(
$fieldValue->getParent()
);
// Ensure we run this after all other field middleware
$fieldValue = $next($fieldValue);
$resolver = $fieldValue->getResolver();
$maxAge = $this->directiveArgValue('maxAge');
$isPrivate = $this->directiveArgValue('private', false);
return $fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($fieldValue, $resolver, $maxAge, $isPrivate) {
$cacheValue = new CacheValue([
'field_value' => $fieldValue,
'root' => $root,
'args' => $args,
'context' => $context,
'resolve_info' => $resolveInfo,
'is_private' => $isPrivate,
]);
$cacheKey = $cacheValue->getKey();
/** @var \Illuminate\Cache\Repository|\Illuminate\Cache\TaggedCache $repository */
$cache = $this->shouldUseTags()
? $this->cacheManager->tags($cacheValue->getTags())
: $this->cacheManager;
$cacheHasKey = $cache->has($cacheKey);
// We found a matching value in the cache, so we can just return early
// without actually running the query
if ($cacheHasKey) {
return $cache->get($cacheKey);
}
$resolvedValue = $resolver($root, $args, $context, $resolveInfo);
$storeInCache = $maxAge
? function ($value) use ($cacheKey, $maxAge, $cache) {
$cache->put($cacheKey, $value, Carbon::now()->addSeconds($maxAge));
}
: function ($value) use ($cacheKey, $cache) {
$cache->forever($cacheKey, $value);
};
($resolvedValue instanceof Deferred)
? $resolvedValue->then(function ($result) use ($storeInCache): void {
$storeInCache($result);
})
: $storeInCache($resolvedValue);
return $resolvedValue;
});
}
/**
* Check if tags should be used and are available.
*
* @return bool
*/
protected function shouldUseTags(): bool
{
return config('lighthouse.cache.tags', false)
&& method_exists($this->cacheManager->store(), 'tags');
}
/**
* Set node's cache key.
*
* @param \Nuwave\Lighthouse\Schema\Values\TypeValue $typeValue
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
protected function setCacheKeyOnParent(TypeValue $typeValue): void
{
if (
// The cache key was already set, so we do not have to look again
$typeValue->getCacheKey()
// The Query type is exempt from requiring a cache key
|| $typeValue->getTypeDefinitionName() === 'Query'
) {
return;
}
/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode $typeDefinition */
$typeDefinition = $typeValue->getTypeDefinition();
// First priority: Look for a field with the @cacheKey directive
/** @var FieldDefinitionNode $field */
foreach ($typeDefinition->fields as $field) {
$hasCacheKey = (new Collection($field->directives))
->contains(function (DirectiveNode $directive): bool {
return $directive->name->value === 'cacheKey';
});
if ($hasCacheKey) {
$typeValue->setCacheKey(
$field->name->value
);
return;
}
}
// Second priority: Look for a Non-Null field with the ID type
/** @var FieldDefinitionNode $field */
foreach ($typeDefinition->fields as $field) {
if (
$field->type instanceof NonNullTypeNode
&& $field->type->type instanceof NamedTypeNode
&& $field->type->type->name->value === 'ID'
) {
$typeValue->setCacheKey(
$field->name->value
);
return;
}
}
throw new DirectiveException(
"No @cacheKey or ID field defined on {$typeValue->getTypeDefinitionName()}"
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class CacheKeyDirective implements Directive, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'cacheKey';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Specify the field to use as a key when creating a cache.
"""
directive @cacheKey on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
}
@@ -0,0 +1,142 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Contracts\Auth\Access\Gate;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class CanDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* @var \Illuminate\Contracts\Auth\Access\Gate
*/
protected $gate;
/**
* CanDirective constructor.
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function __construct(Gate $gate)
{
$this->gate = $gate;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'can';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Check a Laravel Policy to ensure the current user is authorized to access a field.
"""
directive @can(
"""
The ability to check permissions for.
"""
ability: String!
"""
The name of the argument that is used to find a specific model
instance against which the permissions should be checked.
"""
find: String
"""
Additional arguments that are passed to `Gate::check`.
"""
args: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* Ensure the user is authorized to access this field.
*
* @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();
return $next(
$fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver) {
$modelClass = $this->getModelClass();
if ($find = $this->directiveArgValue('find')) {
$modelOrModels = $modelClass::findOrFail($args[$find]);
if ($modelOrModels instanceof Model) {
$modelOrModels = [$modelOrModels];
}
/** @var \Illuminate\Database\Eloquent\Model $model */
foreach ($modelOrModels as $model) {
$this->authorize($context->user(), $model);
}
} else {
$this->authorize($context->user(), $modelClass);
}
return call_user_func_array($previousResolver, func_get_args());
}
)
);
}
/**
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string|\Illuminate\Database\Eloquent\Model $model
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\AuthorizationException
*/
protected function authorize($user, $model): void
{
// The signature of the second argument `$arguments` of `Gate::check`
// should be [modelClassName, additionalArg, additionalArg...]
$arguments = $this->getAdditionalArguments();
array_unshift($arguments, $model);
$can = $this->gate
->forUser($user)
->check(
$this->directiveArgValue('ability'),
$arguments
);
if (! $can) {
throw new AuthorizationException(
"You are not authorized to access {$this->definitionNode->name->value}"
);
}
}
/**
* Additional arguments that are passed to `Gate::check`.
*
* @return mixed[]
*/
protected function getAdditionalArguments(): array
{
return (array) $this->directiveArgValue('args');
}
}
@@ -0,0 +1,75 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Support\Utils;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class ComplexityDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'complexity';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Customize the calculation of a fields complexity score before execution.
"""
directive @complexity(
"""
Reference a function to customize the complexity score calculation.
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
"""
resolver: String
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldFieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldFieldValue, Closure $next): FieldValue
{
if ($this->directiveHasArgument('resolver')) {
[$className, $methodName] = $this->getMethodArgumentParts('resolver');
$namespacedClassName = $this->namespaceClassName(
$className,
$fieldFieldValue->defaultNamespacesForParent()
);
$resolver = Utils::constructResolver($namespacedClassName, $methodName);
} else {
$resolver = function (int $childrenComplexity, array $args): int {
$complexity = Arr::get(
$args,
'first',
Arr::get($args, config('lighthouse.pagination_amount_argument'), 1)
);
return $childrenComplexity * $complexity;
};
}
return $next(
$fieldFieldValue->setComplexity($resolver)
);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\DatabaseManager;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Execution\MutationExecutor;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class CreateDirective extends BaseDirective implements FieldResolver, DefinedDirective
{
/**
* @var \Illuminate\Database\DatabaseManager
*/
protected $databaseManager;
/**
* @param \Illuminate\Database\DatabaseManager $databaseManager
* @return void
*/
public function __construct(DatabaseManager $databaseManager)
{
$this->databaseManager = $databaseManager;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'create';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Create a new Eloquent model with the given arguments.
"""
directive @create(
"""
Specify the class name of the model to use.
This is only needed when the default model resolution does not work.
"""
model: String
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Model {
$modelClassName = $this->getModelClass();
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = new $modelClassName;
$executeMutation = function () use ($model, $args): Model {
return MutationExecutor::executeCreate($model, new Collection($args))->refresh();
};
return config('lighthouse.transactional_mutations', true)
? $this->databaseManager->connection($model->getConnectionName())->transaction($executeMutation)
: $executeMutation();
}
);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class DeleteDirective extends ModifyModelExistenceDirective implements DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'delete';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Delete one or more models by their ID.
The field must have a single non-null argument that may be a list.
"""
directive @delete(
"""
Set to `true` to use global ids for finding the model.
If set to `false`, regular non-global ids are used.
"""
globalId: Boolean = false
"""
Specify the class name of the model to use.
This is only needed when the default model resolution does not work.
"""
model: String
) on FIELD_DEFINITION
SDL;
}
/**
* Find one or more models by id.
*
* @param string|\Illuminate\Database\Eloquent\Model $modelClass
* @param string|int|string[]|int[] $idOrIds
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
*/
protected function find(string $modelClass, $idOrIds)
{
return $modelClass::find($idOrIds);
}
/**
* Bring a model in or out of existence.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
protected function modifyExistence(Model $model): void
{
$model->delete();
}
}
@@ -0,0 +1,55 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use GraphQL\Type\Definition\Directive;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class DeprecatedDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'deprecated';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Marks an element of a GraphQL schema as no longer supported.
"""
directive @deprecated(
"""
Explains why this element was deprecated, usually also including a
suggestion for how to access supported similar data. Formatted
in [Markdown](https://daringfireball.net/projects/markdown/).
"""
reason: String = "No longer supported"
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
$reason = $this->directiveArgValue('reason', Directive::DEFAULT_DEPRECATION_REASON);
$fieldValue->setDeprecationReason($reason);
return $next($fieldValue);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
class EqDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'eq';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @eq(
"""
Specify the database column to compare.
Only required if database column has a different name than the attribute in your schema.
"""
key: String
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
/**
* Apply a "WHERE = $value" clause.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $value
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $value)
{
return $builder->where(
$this->directiveArgValue('key', $this->definitionNode->name->value),
$value
);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
class EventDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $eventsDispatcher;
/**
* Construct EventDirective.
*
* @param \Illuminate\Contracts\Events\Dispatcher $eventsDispatcher
* @return void
*/
public function __construct(EventsDispatcher $eventsDispatcher)
{
$this->eventsDispatcher = $eventsDispatcher;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'event';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Fire an event after a mutation has taken place.
It requires the `dispatch` argument that should be
the class name of the event you want to fire.
"""
directive @event(
"""
Specify the fully qualified class name (FQCN) of the event to dispatch.
"""
dispatch: String!
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
$eventBaseName = $this->directiveArgValue('dispatch');
$eventClassName = $this->namespaceClassName($eventBaseName);
$previousResolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function () use ($previousResolver, $eventClassName) {
$result = call_user_func_array($previousResolver, func_get_args());
$this->eventsDispatcher->dispatch(
new $eventClassName($result)
);
return $result;
}
)
);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Utils;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class FieldDirective extends BaseDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'field';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Assign a resolver function to a field.
"""
directive @field(
"""
A reference to the resolver function to be used.
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
"""
resolver: String!
"""
Supply additional data to the resolver.
"""
args: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
[$className, $methodName] = $this->getMethodArgumentParts('resolver');
$namespacedClassName = $this->namespaceClassName(
$className,
$fieldValue->defaultNamespacesForParent()
);
$resolver = Utils::constructResolver($namespacedClassName, $methodName);
$additionalData = $this->directiveArgValue('args');
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $additionalData) {
return $resolver(
$root,
array_merge($args, ['directive' => $additionalData]),
$context,
$resolveInfo
);
}
);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use GraphQL\Error\Error;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class FindDirective extends BaseDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'find';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Find a model based on the arguments provided.
"""
directive @find(
"""
Specify the class name of the model to use.
This is only needed when the default model resolution does not work.
"""
model: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = $this->getModelClass();
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($model): ?Model {
$results = $resolveInfo
->builder
->addScopes(
$this->directiveArgValue('scopes', [])
)
->apply(
$model::query(),
$args
)
->get();
if ($results->count() > 1) {
throw new Error('The query returned more than one result.');
}
return $results->first();
}
);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class FirstDirective extends BaseDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'first';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Get the first query result from a collection of Eloquent models.
"""
directive @first(
"""
Specify the class name of the model to use.
This is only needed when the default model resolution does not work.
"""
model: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = $this->getModelClass();
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($model): ?Model {
return $resolveInfo
->builder
->addScopes(
$this->directiveArgValue('scopes', [])
)
->apply(
$model::query(),
$args
)
->first();
}
);
}
}
@@ -0,0 +1,113 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GlobalId;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective;
class GlobalIdDirective extends BaseDirective implements FieldMiddleware, ArgTransformerDirective, DefinedDirective
{
/**
* The GlobalId resolver.
*
* @var \Nuwave\Lighthouse\Support\Contracts\GlobalId
*/
protected $globalId;
/**
* GlobalIdDirective constructor.
*
* @param \Nuwave\Lighthouse\Support\Contracts\GlobalId $globalId
* @return void
*/
public function __construct(GlobalId $globalId)
{
$this->globalId = $globalId;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'globalId';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Converts between IDs/types and global IDs.
When used upon a field, it encodes,
when used upon an argument, it decodes.
"""
directive @globalId(
"""
By default, an array of `[$type, $id]` is returned when decoding.
You may limit this to returning just one of both.
Allowed values: "ARRAY", "TYPE", "ID"
"""
decode: String = "ARRAY"
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
SDL;
}
/**
* 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
{
$type = $fieldValue->getParentName();
$resolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function () use ($type, $resolver): string {
$resolvedValue = call_user_func_array($resolver, func_get_args());
return $this->globalId->encode(
$type,
$resolvedValue
);
}
)
);
}
/**
* Decodes a global id given as an argument.
*
* @param string $argumentValue
* @return string|string[]
*/
public function transform($argumentValue)
{
if ($decode = $this->directiveArgValue('decode')) {
switch ($decode) {
case 'TYPE':
return $this->globalId->decodeType($argumentValue);
case 'ID':
return $this->globalId->decodeID($argumentValue);
case 'ARRAY':
return $this->globalId->decode($argumentValue);
default:
throw new DefinitionException(
"The only argument of the @globalId directive can only be ID or TYPE, got {$decode}"
);
}
}
return $this->globalId->decode($argumentValue);
}
}
@@ -0,0 +1,66 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
class HasManyDirective extends RelationDirective implements FieldResolver, FieldManipulator, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'hasMany';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Corresponds to [the Eloquent relationship HasMany](https://laravel.com/docs/eloquent-relationships#one-to-many).
"""
directive @hasMany(
"""
Specify the relationship method name in the model class,
if it is named different from the field in the schema.
"""
relation: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
"""
ALlows to resolve the relation as a paginated list.
Allowed values: paginator, connection.
"""
type: String
"""
Specify the default quantity of elements to be returned.
Only applies when using pagination.
"""
defaultCount: Int
"""
Specify the maximum quantity of elements to be returned.
Only applies when using pagination.
"""
maxCount: Int
"""
Specify a custom type that implements the Edge interface
to extend edge object.
Only applies when using Relay style "connection" pagination.
"""
edgeType: String
) on FIELD_DEFINITION
SDL;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class HasOneDirective extends RelationDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'hasOne';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Corresponds to [the Eloquent relationship HasOne](https://laravel.com/docs/eloquent-relationships#one-to-one).
"""
directive @hasOne(
"""
Specify the relationship method name in the model class,
if it is named different from the field in the schema.
"""
relation: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
) on FIELD_DEFINITION
SDL;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
class InDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'in';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @in(
"""
Specify the database column to compare.
Only required if database column has a different name than the attribute in your schema.
"""
key: String
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
/**
* Apply a simple "WHERE IN $values" clause.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $values
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $values)
{
return $builder->whereIn(
$this->directiveArgValue('key', $this->definitionNode->name->value),
$values
);
}
}
@@ -0,0 +1,87 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Illuminate\Support\Arr;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class InjectDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'inject';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @inject(
"""
A path to the property of the context that will be injected.
If the value is nested within the context, you may use dot notation
to get it, e.g. "user.id".
"""
context: String!
"""
The target name of the argument into which the value is injected.
You can use dot notation to set the value at arbitrary depth
within the incoming argument.
"""
name: String!
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$contextAttributeName = $this->directiveArgValue('context');
if (! $contextAttributeName) {
throw new DirectiveException(
"The `inject` directive on {$fieldValue->getParentName()} [{$fieldValue->getFieldName()}] must have a `context` argument"
);
}
$argumentName = $this->directiveArgValue('name');
if (! $argumentName) {
throw new DirectiveException(
"The `inject` directive on {$fieldValue->getParentName()} [{$fieldValue->getFieldName()}] must have a `name` argument"
);
}
$previousResolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function ($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($contextAttributeName, $argumentName, $previousResolver) {
return $previousResolver(
$rootValue,
Arr::add($args, $argumentName, data_get($context, $contextAttributeName)),
$context,
$resolveInfo
);
}
)
);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class InterfaceDirective extends BaseDirective implements DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'interface';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Use a custom resolver to determine the concrete type of an interface.
"""
directive @interface(
"""
Reference to a custom type-resolver function.
Consists of two parts: a class name and a method name, seperated by an `@` symbol.
If you pass only a class name, the method name defaults to `__invoke`.
"""
resolveType: String!
) on INTERFACE
SDL;
}
}
@@ -0,0 +1,73 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use GraphQL\Deferred;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Collection;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class LazyLoadDirective extends BaseDirective implements DefinedDirective, FieldMiddleware
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'lazyLoad';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Perform a [lazy eager load](https://laravel.com/docs/eloquent-relationships#lazy-eager-loading)
on the relations of a list of models.
"""
directive @lazyLoad(
"""
The names of the relationship methods to load.
"""
relations: [String!]!
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
$relations = $this->directiveArgValue('relations', []);
$resolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $relations) {
/** @var \GraphQL\Deferred|\Illuminate\Database\Eloquent\Model $result */
$result = $resolver($root, $args, $context, $resolveInfo);
($result instanceof Deferred)
? $result->then(function (Collection &$items) use ($relations): Collection {
$items->load($relations);
return $items;
})
: $result->load($relations);
return $result;
}
)
);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class MethodDirective extends BaseDirective implements FieldResolver, DefinedDirective
{
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'method';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Call a method with a given `name` on the class that represents a type to resolve a field.
Use this if the data is not accessible as an attribute (e.g. `$model->myData`).
"""
directive @method(
"""
Specify the method of which to fetch the data from.
"""
name: String
) on FIELD_DEFINITION
SDL;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
/** @var string $method */
$method = $this->directiveArgValue(
'name',
$this->definitionNode->name->value
);
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($method) {
return call_user_func([$root, $method], $root, $args, $context, $resolveInfo);
}
);
}
}
@@ -0,0 +1,201 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Support\Pipeline;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Language\AST\TypeExtensionNode;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Illuminate\Routing\MiddlewareNameResolver;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Compatibility\MiddlewareAdapter;
use Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator;
class MiddlewareDirective extends BaseDirective implements FieldMiddleware, TypeManipulator, TypeExtensionManipulator, DefinedDirective
{
/**
* todo remove as soon as name() is static itself.
* @var string
*/
const NAME = 'middleware';
/**
* @var \Nuwave\Lighthouse\Support\Pipeline
*/
protected $pipeline;
/**
* @var \Nuwave\Lighthouse\Support\Contracts\CreatesContext
*/
protected $createsContext;
/**
* @var \Nuwave\Lighthouse\Support\Compatibility\MiddlewareAdapter
*/
private $middlewareAdapter;
/**
* Create a new middleware directive instance.
*
* @param \Nuwave\Lighthouse\Support\Pipeline $pipeline
* @param \Nuwave\Lighthouse\Support\Contracts\CreatesContext $createsContext
* @param \Nuwave\Lighthouse\Support\Compatibility\MiddlewareAdapter $middlewareAdapter
* @return void
*/
public function __construct(Pipeline $pipeline, CreatesContext $createsContext, MiddlewareAdapter $middlewareAdapter)
{
$this->pipeline = $pipeline;
$this->createsContext = $createsContext;
$this->middlewareAdapter = $middlewareAdapter;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return self::NAME;
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
directive @middleware(
"""
Specify which middleware to run.
Pass in either a fully qualified class name, an alias or
a middleware group - or any combination of them.
"""
checks: [String!]
) on FIELD_DEFINITION
SDL;
}
/**
* 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
{
$middleware = $this->getQualifiedMiddlewareNames(
$this->directiveArgValue('checks')
);
$resolver = $fieldValue->getResolver();
return $next(
$fieldValue->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
);
});
}
)
);
}
/**
* @param string|string[] $middlewareArgValue
* @return \Illuminate\Support\Collection<string>
*/
protected function getQualifiedMiddlewareNames($middlewareArgValue): Collection
{
$middleware = $this->middlewareAdapter->getMiddleware();
$middlewareGroups = $this->middlewareAdapter->getMiddlewareGroups();
return (new Collection($middlewareArgValue))
->map(function (string $name) use ($middleware, $middlewareGroups): array {
return (array) MiddlewareNameResolver::resolve($name, $middleware, $middlewareGroups);
})
->flatten();
}
/**
* Apply manipulations from a type definition node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeDefinitionNode $typeDefinition
* @return void
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
self::addMiddlewareDirectiveToFields($typeDefinition);
}
/**
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\ObjectTypeExtensionNode $objectType
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
public function addMiddlewareDirectiveToFields(&$objectType): void
{
if (
! $objectType instanceof ObjectTypeDefinitionNode
&& ! $objectType instanceof ObjectTypeExtensionNode
) {
throw new DirectiveException(
'The '.self::NAME.' directive may only be placed on fields or object types.'
);
}
$middlewareArgValue = (new Collection($this->directiveArgValue('checks')))
->map(function (string $middleware) : string {
// Add slashes, as re-parsing of the values removes a level of slashes
return addslashes($middleware);
})
->implode('", "');
$middlewareDirective = PartialParser::directive("@middleware(checks: [\"$middlewareArgValue\"])");
/** @var FieldDefinitionNode $fieldDefinition */
foreach ($objectType->fields as $fieldDefinition) {
// 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, self::NAME)) {
return;
}
$fieldDefinition->directives = $fieldDefinition->directives->merge([$middlewareDirective]);
}
}
/**
* Apply manipulations from a type definition node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeExtensionNode $typeExtension
* @return void
*/
public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension): void
{
self::addMiddlewareDirectiveToFields($typeExtension);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Closure;
use GraphQL\Type\Definition\Type;
use Nuwave\Lighthouse\Schema\NodeRegistry;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use GraphQL\Language\AST\TypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Values\TypeValue;
use Nuwave\Lighthouse\Support\Contracts\TypeMiddleware;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class ModelDirective extends BaseDirective implements TypeMiddleware, TypeManipulator, DefinedDirective
{
/**
* @var \Nuwave\Lighthouse\Schema\NodeRegistry
*/
protected $nodeRegistry;
/**
* @param \Nuwave\Lighthouse\Schema\NodeRegistry $nodeRegistry
* @return void
*/
public function __construct(NodeRegistry $nodeRegistry)
{
$this->nodeRegistry = $nodeRegistry;
}
/**
* Directive name.
*
* @return string
*/
public function name(): string
{
return 'model';
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Enable fetching an Eloquent model by its global id through the `node` query.
"""
directive @model on OBJECT
SDL;
}
/**
* Handle type construction.
*
* @param \Nuwave\Lighthouse\Schema\Values\TypeValue $value
* @param \Closure $next
* @return \GraphQL\Type\Definition\Type
*/
public function handleNode(TypeValue $value, Closure $next): Type
{
$this->nodeRegistry->registerModel(
$value->getTypeDefinitionName(),
$this->getModelClass('class')
);
return $next($value);
}
/**
* Apply manipulations from a type definition node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeDefinitionNode $typeDefinition
* @return void
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
ASTHelper::attachNodeInterfaceToObjectType($typeDefinition);
}
}
@@ -0,0 +1,146 @@
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use GraphQL\Language\AST\ListTypeNode;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Language\AST\NonNullTypeNode;
use Illuminate\Database\Eloquent\Collection;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\GlobalId;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
abstract class ModifyModelExistenceDirective extends BaseDirective implements FieldResolver, FieldManipulator, DefinedDirective
{
/**
* The GlobalId resolver.
*
* @var \Nuwave\Lighthouse\Support\Contracts\GlobalId
*/
protected $globalId;
/**
* DeleteDirective constructor.
*
* @param \Nuwave\Lighthouse\Support\Contracts\GlobalId $globalId
* @return void
*/
public function __construct(GlobalId $globalId)
{
$this->globalId = $globalId;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args) {
/** @var string|int|string[]|int[] $idOrIds */
$idOrIds = reset($args);
if ($this->directiveArgValue('globalId', false)) {
// At this point we know the type is at least wrapped in a NonNull type, so we go one deeper
if ($this->idArgument()->type instanceof ListTypeNode) {
$idOrIds = array_map(
function (string $id): string {
return $this->globalId->decodeID($id);
},
$idOrIds
);
} else {
$idOrIds = $this->globalId->decodeID($idOrIds);
}
}
$modelOrModels = $this->find(
$this->getModelClass(),
$idOrIds
);
if (! $modelOrModels) {
return;
}
if ($modelOrModels instanceof Model) {
$this->modifyExistence($modelOrModels);
}
if ($modelOrModels instanceof Collection) {
foreach ($modelOrModels as $model) {
$this->modifyExistence($model);
}
}
return $modelOrModels;
}
);
}
/**
* Get the type of the id argument.
*
* Not using an actual type hint, as the manipulateFieldDefinition function
* validates the type during schema build time.f
*
* @return \GraphQL\Language\AST\NonNullTypeNode
*/
protected function idArgument()
{
return $this->definitionNode->arguments[0]->type;
}
/**
* @param DocumentAST $documentAST
* @param FieldDefinitionNode $fieldDefinition
* @param ObjectTypeDefinitionNode $parentType
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
public function manipulateFieldDefinition(
DocumentAST &$documentAST,
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType
): void {
// Ensure there is only a single argument defined on the field.
if (count($this->definitionNode->arguments) !== 1) {
throw new DirectiveException(
'The @'.static::name()." directive requires the field {$this->definitionNode->name->value} to only contain a single argument."
);
}
if (! $this->idArgument() instanceof NonNullTypeNode) {
throw new DirectiveException(
'The @'.static::name()." directive requires the field {$this->definitionNode->name->value} to have a NonNull argument. Mark it with !"
);
}
}
/**
* Find one or more models by id.
*
* @param string|\Illuminate\Database\Eloquent\Model $modelClass
* @param string|int|string[]|int[] $idOrIds
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
*/
abstract protected function find(string $modelClass, $idOrIds);
/**
* Bring a model in or out of existence.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
abstract protected function modifyExistence(Model $model): void;
}

Some files were not shown because too many files have changed in this diff Show More