Vendor
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
+146
@@ -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
Reference in New Issue
Block a user