diff --git a/.php_cs b/.php_cs index 19a02d5dc1..06d57b9a90 100644 --- a/.php_cs +++ b/.php_cs @@ -18,4 +18,5 @@ return PhpCsFixer\Config::create() 'no_unused_imports' => false, 'ordered_imports' => true, 'phpdoc_order' => true, + 'not_operator_with_successor_space' => true ])->setFinder($finder); diff --git a/assets/node.graphql b/assets/node.graphql deleted file mode 100644 index d36f0843e1..0000000000 --- a/assets/node.graphql +++ /dev/null @@ -1,5 +0,0 @@ -# Node global interface -interface Node @interface(resolver: "Nuwave\\Lighthouse\\Support\\Http\\GraphQL\\Interfaces\\NodeInterface@resolve") { - # Global identifier that can be used to resolve any Node implementation. - _id: ID! -} diff --git a/composer.json b/composer.json index 16840579ca..c55ba3a889 100644 --- a/composer.json +++ b/composer.json @@ -49,8 +49,11 @@ } }, "scripts": { - "test" : "vendor/bin/phpunit --colors=always", - "test:ci": "composer test -- --verbose --coverage-text --coverage-clover=coverage.xml" + "test" : "phpunit --colors=always", + "test:unit" : "phpunit --colors=always --testsuite Unit", + "test:integration" : "phpunit --colors=always --testsuite Integration", + "test:ci": "phpunit --colors=always --verbose --coverage-text --coverage-clover=coverage.xml", + "style": "php-cs-fixer fix" }, "extra": { "laravel": { diff --git a/config/config.php b/config/config.php index 03822bc01d..8591e28320 100644 --- a/config/config.php +++ b/config/config.php @@ -22,15 +22,14 @@ // 'middleware' => ['web','api'], // [ 'loghttp'] ], - /* |-------------------------------------------------------------------------- | Directive registry |-------------------------------------------------------------------------- | - | This package allows you to create your own server-side directives. Change - | these values to register the directory that will hold all of your - | custom directives. + | This package allows you to create your own server-side directives. + | List directories that will be scanned for custom directives. + | Hint: Directives must implement \Nuwave\Lighthouse\Schema\Directives\Directive | */ 'directives' => [__DIR__.'/../app/Http/GraphQL/Directives'], @@ -67,10 +66,14 @@ | Schema Cache |-------------------------------------------------------------------------- | - | Specify where the GraphQL schema should be cached. + | A large part of the Schema generation is parsing into an AST. + | This operation is cached by default when APP_ENV is set to 'production' | */ - 'cache' => null, + 'cache' => [ + 'enable' => true, + 'key' => 'lighthouse-schema', + ], /* |-------------------------------------------------------------------------- diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 36edc49fcb..3664e85dcd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,12 +8,14 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" verbose="true" > - - ./tests/ + + ./tests/Unit/ + + + ./tests/Integration/ @@ -27,7 +29,7 @@ - + diff --git a/src/GraphQL.php b/src/GraphQL.php index de5ccb9057..c3b13128ac 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -4,25 +4,20 @@ use GraphQL\GraphQL as GraphQLBase; use GraphQL\Type\Schema; -use Nuwave\Lighthouse\Schema\CacheManager; -use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory; +use Nuwave\Lighthouse\Schema\AST\ASTBuilder; +use Nuwave\Lighthouse\Schema\AST\DocumentAST; +use Nuwave\Lighthouse\Schema\AST\SchemaStitcher; +use Nuwave\Lighthouse\Schema\DirectiveRegistry; use Nuwave\Lighthouse\Schema\MiddlewareManager; use Nuwave\Lighthouse\Schema\NodeContainer; use Nuwave\Lighthouse\Schema\SchemaBuilder; -use Nuwave\Lighthouse\Schema\Utils\SchemaStitcher; +use Nuwave\Lighthouse\Schema\TypeRegistry; use Nuwave\Lighthouse\Support\Traits\CanFormatError; class GraphQL { use CanFormatError; - /** - * Cache manager. - * - * @var CacheManager - */ - protected $cache; - /** * Schema builder. * @@ -31,12 +26,26 @@ class GraphQL protected $schema; /** - * Directive container. + * Directive registry container. * - * @var DirectiveFactory + * @var DirectiveRegistry */ protected $directives; + /** + * Type registry container. + * + * @var TypeRegistry + */ + protected $types; + + /** + * Instance of node container. + * + * @var NodeContainer + */ + protected $nodes; + /** * Middleware manager. * @@ -58,12 +67,32 @@ class GraphQL */ protected $graphqlSchema; + /** + * Create instance of graphql container. + * + * @param DirectiveRegistry $directives + * @param TypeRegistry $types + * @param MiddlewareManager $middleware + * @param NodeContainer $nodes + */ + public function __construct( + DirectiveRegistry $directives, + TypeRegistry $types, + MiddlewareManager $middleware, + NodeContainer $nodes + ) { + $this->directives = $directives; + $this->types = $types; + $this->middleware = $middleware; + $this->nodes = $nodes; + } + /** * Prepare graphql schema. */ public function prepSchema() { - $this->graphqlSchema = $this->graphqlSchema ?: $this->buildSchema(); + return $this->graphqlSchema = $this->graphqlSchema ?: $this->buildSchema(); } /** @@ -124,20 +153,43 @@ public function queryAndReturnResult($query, $context = null, $variables = [], $ } /** - * Build a new schema instance. + * Build a new executable schema. * * @return Schema */ public function buildSchema() { - $schema = $this->cache()->get(function () { - return $this->stitcher()->stitch( - config('lighthouse.global_id_field', '_id'), - config('lighthouse.schema.register') - ); - }); - - return $this->schema()->build($schema); + $documentAST = $this->shouldCacheAST() + ? Cache::rememberForever(config('lighthouse.cache.key'), function () { + return $this->buildAST(); + }) + : $this->buildAST(); + + return (new SchemaBuilder())->build($documentAST); + } + + /** + * Determine if the AST should be cached. + * + * @return bool + */ + protected function shouldCacheAST() + { + return app()->environment('production') && config('cache.enable'); + } + + /** + * Get the stitched schema and build an AST out of it. + * + * @return DocumentAST + */ + protected function buildAST() + { + $schemaString = SchemaStitcher::stitch( + config('lighthouse.schema.register') + ); + + return ASTBuilder::generate($schemaString); } /** @@ -163,71 +215,41 @@ public function batch($abstract, $key, array $data = [], $name = null) /** * Get an instance of the schema builder. * - * @return SchemaBuilder + * @return TypeRegistry */ public function schema() { - if (! $this->schema) { - $this->schema = app(SchemaBuilder::class); - } - - return $this->schema; + return $this->types(); } /** * Get an instance of the directive container. * - * @return DirectiveFactory + * @return DirectiveRegistry */ public function directives() { - if (! $this->directives) { - $this->directives = app(DirectiveFactory::class); - } - return $this->directives; } /** - * Get instance of middle manager. + * Get instance of type container. * - * @return MiddlewareManager + * @return TypeRegistry */ - public function middleware() + public function types() { - if (! $this->middleware) { - $this->middleware = app(MiddlewareManager::class); - } - - return $this->middleware; + return $this->types; } /** - * Get instance of cache manager. - * - * @return CacheManager - */ - public function cache() - { - if (! $this->cache) { - $this->cache = app(CacheManager::class); - } - - return $this->cache; - } - - /** - * Get instance of schema stitcher. + * Get instance of middle manager. * - * @return SchemaStitcher + * @return MiddlewareManager */ - public function stitcher() + public function middleware() { - if (! $this->stitcher) { - $this->stitcher = app(SchemaStitcher::class); - } - - return $this->stitcher; + return $this->middleware; } /** @@ -237,13 +259,6 @@ public function stitcher() */ public function nodes() { - if (! app()->has(NodeContainer::class)) { - return app()->instance( - NodeContainer::class, - resolve(NodeContainer::class) - ); - } - - return resolve(NodeContainer::class); + return $this->nodes; } } diff --git a/src/Providers/LighthouseServiceProvider.php b/src/Providers/LighthouseServiceProvider.php index 1b2006c6c4..5bb02c9ed3 100644 --- a/src/Providers/LighthouseServiceProvider.php +++ b/src/Providers/LighthouseServiceProvider.php @@ -22,16 +22,22 @@ public function boot() $this->loadRoutesFrom(__DIR__.'/../Support/Http/routes.php'); } - $this->registerSchema(); $this->registerMacros(); } + /** + * Load routes from provided path. + * + * @param string $path + */ protected function loadRoutesFrom($path) { - if(Str::contains($this->app->version(), "Lumen")) { - require realpath($path); - return; + if (Str::contains($this->app->version(), 'Lumen')) { + require realpath($path); + + return; } + parent::loadRoutesFrom($path); } @@ -40,33 +46,14 @@ protected function loadRoutesFrom($path) */ public function register() { - $this->app->singleton('graphql', function () { - return new GraphQL(); - }); - - $this->app->alias('graphql', GraphQL::class); + $this->app->singleton(GraphQL::class); + $this->app->alias(GraphQL::class, 'graphql'); if ($this->app->runningInConsole()) { - $this->commands([ - \Nuwave\Lighthouse\Support\Console\Commands\CacheCommand::class, - ]); + $this->commands([]); } } - /** - * Register GraphQL schema. - */ - public function registerSchema() - { - directives()->load(realpath(__DIR__.'/../Schema/Directives/'), 'Nuwave\\Lighthouse\\'); - directives()->load(config('lighthouse.directives', [])); - - graphql()->stitcher()->stitch( - config('lighthouse.global_id_field', '_id'), - config('lighthouse.schema.register') - ); - } - /** * Register lighthouse macros. */ diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php new file mode 100644 index 0000000000..c6a4f5f3d9 --- /dev/null +++ b/src/Schema/AST/ASTBuilder.php @@ -0,0 +1,200 @@ +typeExtensions() + // Unwrap extended types so they can be treated same as other types + ->map(function (TypeExtensionDefinitionNode $typeExtension) { + return $typeExtension->definition; + }) + // This is just temporarily merged together + ->concat($document->objectTypes()) + ->reduce(function (DocumentAST $document, ObjectTypeDefinitionNode $objectType) use ( + $originalDocument + ) { + $nodeManipulators = graphql()->directives()->nodeManipulators($objectType); + + return $nodeManipulators->reduce(function (DocumentAST $document, NodeManipulator $nodeManipulator) use ( + $originalDocument, + $objectType + ) { + return $nodeManipulator->manipulateSchema($objectType, $document, $originalDocument); + }, $document); + }, $document); + } + + protected static function mergeTypeExtensions(DocumentAST $document) + { + $document->objectTypes()->each(function (ObjectTypeDefinitionNode $objectType) use ($document) { + $name = $objectType->name->value; + + $document->typeExtensions($name)->reduce(function ( + ObjectTypeDefinitionNode $relatedObjectType, + TypeExtensionDefinitionNode $typeExtension + ) { + /** @var NodeList $fields */ + $fields = $relatedObjectType->fields; + $relatedObjectType->fields = $fields->merge($typeExtension->definition->fields); + + return $relatedObjectType; + }, $objectType); + + // Modify the original document by overwriting the definition with the merged one + $document->setDefinition($objectType); + }); + + return $document; + } + + /** + * @param DocumentAST $document + * + * @return DocumentAST + */ + protected static function applyFieldManipulators(DocumentAST $document) + { + $originalDocument = $document; + + return $document->objectTypes()->reduce(function ( + DocumentAST $document, + ObjectTypeDefinitionNode $objectType + ) use ($originalDocument) { + return collect($objectType->fields)->reduce(function ( + DocumentAST $document, + FieldDefinitionNode $fieldDefinition + ) use ($objectType, $originalDocument) { + $fieldManipulators = graphql()->directives()->fieldManipulators($fieldDefinition); + + return $fieldManipulators->reduce(function ( + DocumentAST $document, + FieldManipulator $fieldManipulator + ) use ($fieldDefinition, $objectType, $originalDocument) { + return $fieldManipulator->manipulateSchema($fieldDefinition, $objectType, $document, + $originalDocument); + }, $document); + }, $document); + }, $document); + } + + /** + * @param DocumentAST $document + * + * @return DocumentAST + */ + protected static function applyArgManipulators(DocumentAST $document) + { + $originalDocument = $document; + + return $document->objectTypes()->reduce( + function (DocumentAST $document, ObjectTypeDefinitionNode $parentType) use ($originalDocument) { + return collect($parentType->fields)->reduce( + function (DocumentAST $document, FieldDefinitionNode $parentField) use ( + $parentType, + $originalDocument + ) { + return collect($parentField->arguments)->reduce( + function (DocumentAST $document, InputValueDefinitionNode $argDefinition) use ( + $parentType, + $parentField, + $originalDocument + ) { + $argManipulators = graphql()->directives()->argManipulators($argDefinition); + + return $argManipulators->reduce( + function (DocumentAST $document, ArgManipulator $argManipulator) use ( + $argDefinition, + $parentField, + $parentType, + $originalDocument + ) { + return $argManipulator->manipulateSchema($argDefinition, $parentField, + $parentType, $document, $originalDocument); + }, $document); + }, $document); + }, $document); + }, $document); + } + + /** + * Inject the node type and a node field into Query. + * + * @param DocumentAST $document + * + * @return DocumentAST + * @throws \Nuwave\Lighthouse\Support\Exceptions\ParseException + */ + protected static function addNodeSupport(DocumentAST $document) + { + $hasTypeImplementingNode = $document->objectTypes()->contains(function (ObjectTypeDefinitionNode $objectType) { + return collect($objectType->interfaces)->contains(function (NamedTypeNode $interface) { + return 'Node' === $interface->name->value; + }); + }); + + // Only add the node type and node field if a type actually implements them + // Otherwise, a validation error is thrown + if (!$hasTypeImplementingNode) { + return $document; + } + + $globalId = config('lighthouse.global_id_field', '_id'); + + // Double slashes to escape the slashes in the namespace. + $interface = PartialParser::interfaceTypeDefinition(" + # Node global interface + interface Node @interface(resolver: \"Nuwave\\\\Lighthouse\\\\Support\\\\Http\\\\GraphQL\\\\Interfaces\\\\NodeInterface@resolve\") { + # Global identifier that can be used to resolve any Node implementation. + $globalId: ID! + } + "); + $document->setDefinition($interface); + + $nodeQuery = PartialParser::fieldDefinition('node(id: ID!): Node @field(resolver: "Nuwave\\\Lighthouse\\\Support\\\Http\\\GraphQL\\\Queries\\\NodeQuery@resolve")'); + $document->addFieldToQueryType($nodeQuery); + + return $document; + } +} diff --git a/src/Schema/AST/ASTHelper.php b/src/Schema/AST/ASTHelper.php new file mode 100644 index 0000000000..74ef0a033b --- /dev/null +++ b/src/Schema/AST/ASTHelper.php @@ -0,0 +1,32 @@ +merge($addition); + } +} diff --git a/src/Schema/AST/DocumentAST.php b/src/Schema/AST/DocumentAST.php new file mode 100644 index 0000000000..efeb6f502a --- /dev/null +++ b/src/Schema/AST/DocumentAST.php @@ -0,0 +1,269 @@ +documentNode = $documentNode; + } + + /** + * Create a new instance from a schema. + * + * @param $schema + * + * @return DocumentAST + */ + public static function fromSource($schema) + { + return new static(Parser::parse($schema)); + } + + /** + * Get a collection of the contained definitions. + * + * @return Collection + */ + public function definitions() + { + return collect($this->documentNode->definitions); + } + + /** + * Get all type definitions from the document. + * + * @return Collection + */ + public function typeDefinitions() + { + return $this->definitions()->filter(function (DefinitionNode $node) { + return $node instanceof ScalarTypeDefinitionNode + || $node instanceof ObjectTypeDefinitionNode + || $node instanceof InterfaceTypeDefinitionNode + || $node instanceof UnionTypeDefinitionNode + || $node instanceof EnumTypeDefinitionNode + || $node instanceof InputObjectTypeDefinitionNode; + }); + } + + /** + * Get all definitions for directives. + * + * @return Collection + */ + public function directives() + { + return $this->definitionsByType(DirectiveDefinitionNode::class); + } + + /** + * Get all definitions for type extensions. + * + * Without a name, it simply return all TypeExtensions. + * If a name is given, it may return multiple type extensions + * that apply to a named type. + * + * @param string|null $extendedTypeName + * + * @return Collection + */ + public function typeExtensions($extendedTypeName = null) + { + return $this->definitionsByType(TypeExtensionDefinitionNode::class) + ->filter(function (TypeExtensionDefinitionNode $typeExtension) use ($extendedTypeName) { + return is_null($extendedTypeName) || $extendedTypeName === $typeExtension->definition->name->value; + }); + } + + /** + * Get all definitions for operations. + * + * @return Collection + */ + public function operations() + { + return $this->definitionsByType(OperationDefinitionNode::class); + } + + /** + * Get all fragment definitions. + * + * @return Collection + */ + public function fragments() + { + return $this->definitionsByType(FragmentDefinitionNode::class); + } + + /** + * Get all definitions for object types. + * + * @return Collection + */ + public function objectTypes() + { + return $this->definitionsByType(ObjectTypeDefinitionNode::class); + } + + /** + * Get all interface definitions. + * + * @return Collection + */ + public function interfaces() + { + return $this->definitionsByType(InterfaceTypeDefinitionNode::class); + } + + /** + * Get the root query type definition. + * + * @return ObjectTypeDefinitionNode + */ + public function queryType() + { + return $this->objectTypeOrDefault('Query'); + } + + /** + * Get the root mutation type definition. + * + * @return ObjectTypeDefinitionNode + */ + public function mutationType() + { + return $this->objectTypeOrDefault('Mutation'); + } + + /** + * Get the root subscription type definition. + * + * @return ObjectTypeDefinitionNode + */ + public function subscriptionType() + { + return $this->objectTypeOrDefault('Subscription'); + } + + /** + * Either get an existing definition or an empty type definition. + * + * @param string $name + * + * @return ObjectTypeDefinitionNode + */ + protected function objectTypeOrDefault($name) + { + return $this->objectType($name) + ?: PartialParser::objectTypeDefinition('type '.$name.'{}'); + } + + /** + * @param string $name + * + * @return ObjectTypeDefinitionNode|null + */ + public function objectType($name) + { + return $this->objectTypes()->first(function (ObjectTypeDefinitionNode $objectType) use ($name) { + return $objectType->name->value === $name; + }); + } + + /** + * @param string $type + * + * @return Collection + */ + protected function definitionsByType($type) + { + return $this->definitions()->filter(function ($node) use ($type) { + return $node instanceof $type; + }); + } + + /** + * Add a single field to the query type. + * + * @param FieldDefinitionNode $field + * + * @return $this + */ + public function addFieldToQueryType(FieldDefinitionNode $field) + { + $query = $this->queryType(); + $query->fields = $query->fields->merge([$field]); + $this->setDefinition($query); + + return $this; + } + + /** + * @param DefinitionNode $definition + * + * @return DocumentAST + */ + public function setDefinition(DefinitionNode $definition) + { + $newName = $definition->name->value; + $newDefinitions = $this->definitions() + ->reject(function (DefinitionNode $node) use ($newName) { + $nodeName = data_get($node, 'name.value'); + // We only consider replacing nodes that have a name + // We can safely kick this by name because names must be unique + return $nodeName && $nodeName === $newName; + })->push($definition) + // Reindex, otherwise offset errors might happen in subsequent runs + ->values() + ->all(); + + // This was a NodeList before, so put it back as it was + $this->documentNode->definitions = new NodeList($newDefinitions); + + return $this; + } + + /** + * @param string $definition + * + * @throws \Exception + * + * @return static + */ + public function setObjectTypeFromString($definition) + { + $objectType = self::parseObjectType($definition); + $this->setDefinition($objectType); + + return $this; + } +} diff --git a/src/Schema/AST/PartialParser.php b/src/Schema/AST/PartialParser.php new file mode 100644 index 0000000000..ebc73072c9 --- /dev/null +++ b/src/Schema/AST/PartialParser.php @@ -0,0 +1,287 @@ +definitions, + ObjectTypeDefinitionNode::class + ); + } + + /** + * @param string $inputValueDefinition + * + * @throws ParseException + * + * @return InputValueDefinitionNode + */ + public static function inputValueDefinition($inputValueDefinition) + { + return self::getFirstAndValidateType( + self::fieldDefinition("field($inputValueDefinition): String")->arguments, + InputValueDefinitionNode::class + ); + } + + /** + * @param string[] $inputValueDefinitions + * + * @return InputValueDefinitionNode[] + */ + public static function inputValueDefinitions($inputValueDefinitions) + { + return array_map(function ($inputValueDefinition) { + return self::inputValueDefinition($inputValueDefinition); + }, $inputValueDefinitions); + } + + /** + * @param string $argumentDefinition + * + * @throws ParseException + * + * @return NodeList + */ + public static function argument($argumentDefinition) + { + return self::getFirstAndValidateType( + self::field("field($argumentDefinition): String")->arguments, + ArgumentNode::class + ); + } + + /** + * @param string[] $argumentDefinitions + * + * @return InputValueDefinitionNode[] + */ + public static function arguments($argumentDefinitions) + { + return array_map(function ($argumentDefinition) { + return self::argument($argumentDefinition); + }, $argumentDefinitions); + } + + /** + * @param string $field + * + * @throws ParseException + * + * @return FieldNode + */ + public static function field($field) + { + return self::getFirstAndValidateType( + self::operationDefinition("{ $field }")->selectionSet->selections, + FieldNode::class + ); + } + + /** + * @param string $operation + * + * @throws ParseException + * + * @return OperationDefinitionNode + */ + public static function operationDefinition($operation) + { + return self::getFirstAndValidateType( + Parser::parse($operation)->definitions, + OperationDefinitionNode::class + ); + } + + /** + * @param string $fieldDefinition + * + * @throws ParseException + * + * @return FieldDefinitionNode + */ + public static function fieldDefinition($fieldDefinition) + { + return self::getFirstAndValidateType( + self::objectTypeDefinition("type Dummy { $fieldDefinition }")->fields, + FieldDefinitionNode::class + ); + } + + /** + * @param string $directive + * + * @throws ParseException + * + * @return DirectiveNode + */ + public static function directive($directive) + { + return self::getFirstAndValidateType( + self::objectTypeDefinition("type Dummy $directive {}")->directives, + DirectiveNode::class + ); + } + + /** + * @param string[] $directives + * + * @return DirectiveNode[] + */ + public static function directives($directives) + { + return array_map(function ($directive) { + return self::inputValueDefinition($directive); + }, $directives); + } + + /** + * @param string $directiveDefinition + * + * @throws ParseException + * + * @return DirectiveDefinitionNode + */ + public static function directiveDefinition($directiveDefinition) + { + return self::getFirstAndValidateType( + Parser::parse($directiveDefinition)->definitions, + DirectiveDefinitionNode::class + ); + } + + /** + * @param string[] $directiveDefinitions + * + * @return DirectiveDefinitionNode[] + */ + public static function directiveDefinitions($directiveDefinitions) + { + return array_map(function ($directiveDefinition) { + return self::inputValueDefinition($directiveDefinition); + }, $directiveDefinitions); + } + + /** + * @param $interfaceDefinition + * + * @throws ParseException + * + * @return InterfaceTypeDefinitionNode + */ + public static function interfaceTypeDefinition($interfaceDefinition) + { + return self::getFirstAndValidateType( + Parser::parse($interfaceDefinition)->definitions, + InterfaceTypeDefinitionNode::class + ); + } + + /** + * @param string $inputTypeDefinition + * + * @throws ParseException + * + * @return InputObjectTypeDefinitionNode + */ + public static function inputObjectTypeDefinition($inputTypeDefinition) + { + return self::getFirstAndValidateType( + Parser::parse($inputTypeDefinition)->definitions, + InputObjectTypeDefinitionNode::class + ); + } + + /** + * @param string $scalarDefinition + * + * @throws ParseException + * + * @return ScalarTypeDefinitionNode + */ + public static function scalarTypeDefinition($scalarDefinition) + { + return self::getFirstAndValidateType( + Parser::parse($scalarDefinition)->definitions, + ScalarTypeDefinitionNode::class + ); + } + + /** + * @param $enumDefinition + * + * @throws ParseException + * + * @return mixed + */ + public static function enumTypeDefinition($enumDefinition) + { + return self::getFirstAndValidateType( + Parser::parse($enumDefinition)->definitions, + EnumTypeDefinitionNode::class + ); + } + + /** + * Get the first Node from a given NodeList and validate it. + * + * @param NodeList $list + * @param string $expectedType + * + * @throws ParseException + * + * @return mixed + */ + protected static function getFirstAndValidateType(NodeList $list, $expectedType) + { + if (1 !== $list->count()) { + throw new ParseException(' More than one definition was found in the passed in schema.'); + } + + $node = $list[0]; + + if (! $node instanceof $expectedType) { + throw new ParseException("The given definition was not of type: $expectedType"); + } + + return $node; + } +} diff --git a/src/Schema/Utils/SchemaStitcher.php b/src/Schema/AST/SchemaStitcher.php similarity index 50% rename from src/Schema/Utils/SchemaStitcher.php rename to src/Schema/AST/SchemaStitcher.php index 3e5754a90a..a930037888 100644 --- a/src/Schema/Utils/SchemaStitcher.php +++ b/src/Schema/AST/SchemaStitcher.php @@ -1,43 +1,23 @@ lighthouseSchema($globalId); - $app = $path ? $this->appSchema($path) : ''; + $lighthouse = file_get_contents(realpath(__DIR__.'/../../../assets/schema.graphql')); - return $lighthouse.$app; - } + $app = $path ? self::appSchema($path) : ''; - /** - * Get Lighthouse schema. - * - * @param string $globalId - * - * @return string - */ - public function lighthouseSchema($globalId = '_id') - { - $schema = file_get_contents(realpath(__DIR__.'/../../../assets/schema.graphql')); - - if ($globalId) { - $node = file_get_contents(realpath(__DIR__.'/../../../assets/node.graphql')); - - return str_replace('_id', $globalId, $node).$schema; - } - - return $schema; + return $lighthouse.$app; } /** @@ -47,7 +27,7 @@ public function lighthouseSchema($globalId = '_id') * * @return string */ - protected function appSchema($path) + protected static function appSchema($path) { try { $schema = file_get_contents($path); @@ -61,7 +41,7 @@ protected function appSchema($path) })->map(function ($import) { return trim(str_replace('#import', '', $import)); })->map(function ($file) use ($path) { - return $this->appSchema(realpath(dirname($path).'/'.$file)); + return self::appSchema(realpath(dirname($path).'/'.$file)); })->implode("\n"); return $imports."\n".$schema; diff --git a/src/Schema/CacheManager.php b/src/Schema/CacheManager.php deleted file mode 100644 index b97b806c37..0000000000 --- a/src/Schema/CacheManager.php +++ /dev/null @@ -1,52 +0,0 @@ -parseSchema($schema); - - file_put_contents( - config('lighthouse.cache'), - "set($schema()); - } -} diff --git a/src/Schema/DirectiveRegistry.php b/src/Schema/DirectiveRegistry.php new file mode 100644 index 0000000000..fa0c04d07f --- /dev/null +++ b/src/Schema/DirectiveRegistry.php @@ -0,0 +1,370 @@ +directives = collect(); + + // Load built-in directives from the default directory + $this->load(realpath(__DIR__ . '/Directives/'), 'Nuwave\\Lighthouse\\'); + + // Load custom directives + $this->load(config('lighthouse.directives', [])); + } + + /** + * Gather all directives from a given directory and register them. + * + * Works similar to https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Console/Kernel.php#L191-L225 + * + * @param array|string $paths + * @param null $namespace + */ + protected function load($paths, $namespace = null) + { + $paths = collect($paths) + ->unique() + ->filter(function ($path) { + return is_dir($path); + })->map(function ($path) { + return realpath($path); + })->all(); + + if (empty($paths)) { + return; + } + + $namespace = $namespace ?: app()->getNamespace(); + $path = starts_with($namespace, 'Nuwave\\Lighthouse') + ? realpath(__DIR__ . '/../../src/') + : app_path(); + + /** @var SplFileInfo $file */ + foreach ((new Finder())->in($paths)->files() as $file) { + $className = $namespace . str_replace( + ['/', '.php'], + ['\\', ''], + str_after($file->getPathname(), $path . DIRECTORY_SEPARATOR) + ); + + $this->tryRegisterClassName($className); + } + } + + /** + * Register a directive class. + * + * @param string $className + * + * @throws \ReflectionException + */ + protected function tryRegisterClassName($className) + { + $reflection = new \ReflectionClass($className); + + if ($reflection->isInstantiable() && $reflection->isSubclassOf(Directive::class)) { + $directive = $reflection->newInstance(); + $this->register($directive); + } + } + + /** + * Register a directive. + * + * @param Directive $directive + */ + public function register(Directive $directive) + { + $this->directives->put($directive->name(), $directive); + } + + /** + * Get directive instance by name. + * + * @param string $name + * + * @throws DirectiveException + * + * @return Directive + */ + public function get($name) + { + $handler = $this->directives->get($name); + + if (!$handler) { + throw new DirectiveException("No directive has been registered for [{$name}]"); + } + + return $handler; + } + + /** + * Get directive instance by name. + * + * @param string $name + * + * @throws DirectiveException + * + * @return Directive + * + * @deprecated Will be removed in next major release + */ + public function handler($name) + { + return $this->get($name); + } + + /** + * Get all directives associated with a node. + * + * @param Node $node + * + * @return \Illuminate\Support\Collection + */ + protected function directives(Node $node) + { + return collect(data_get($node, 'directives', []))->map(function (DirectiveNode $directive) { + return $this->get($directive->name->value); + }); + } + + /** + * @param Node $node + * + * @return \Illuminate\Support\Collection + */ + public function nodeManipulators(Node $node) + { + return $this->directives($node)->filter(function (Directive $directive) { + return $directive instanceof NodeManipulator; + }); + } + + /** + * @param FieldDefinitionNode $fieldDefinition + * + * @return \Illuminate\Support\Collection + */ + public function fieldManipulators(FieldDefinitionNode $fieldDefinition) + { + return $this->directives($fieldDefinition)->filter(function (Directive $directive) { + return $directive instanceof FieldManipulator; + })->map(function (FieldManipulator $directive) use ($fieldDefinition) { + return $this->hydrate($directive, $fieldDefinition); + }); + } + + /** + * @param $inputValueDefinition + * + * @return \Illuminate\Support\Collection + */ + public function argManipulators(InputValueDefinitionNode $inputValueDefinition) + { + return $this->directives($inputValueDefinition)->filter(function (Directive $directive) { + return $directive instanceof ArgManipulator; + }); + } + + /** + * Get the node resolver directive for the given type definition. + * + * @param Node $node + * + * @return NodeResolver + */ + public function forNode(Node $node) + { + return $this->nodeResolver($node); + } + + /** + * Get the node resolver directive for the given type definition. + * + * @param Node $node + * + * @return NodeResolver + */ + public function nodeResolver(Node $node) + { + $resolvers = $this->directives($node)->filter(function (Directive $directive) { + return $directive instanceof NodeResolver; + }); + + if ($resolvers->count() > 1) { + $nodeName = data_get($node, 'name.value'); + throw new DirectiveException("Node $nodeName can only have one NodeResolver directive. Check your schema definition"); + } + + return $resolvers->first(); + } + + /** + * Check if the given node has a type resolver directive handler assigned to it. + * + * @param Node $typeDefinition + * + * @return bool + */ + public function hasNodeResolver(Node $typeDefinition) + { + return $this->nodeResolver($typeDefinition) instanceof NodeResolver; + } + + /** + * @param FieldDefinitionNode $fieldDefinition + * + * @return bool + */ + public function hasResolver($fieldDefinition) + { + return $this->hasFieldResolver($fieldDefinition); + } + + /** + * Check if the given field has a field resolver directive handler assigned to it. + * + * @param FieldDefinitionNode $fieldDefinition + * + * @return bool + */ + public function hasFieldResolver($fieldDefinition) + { + return $this->fieldResolver($fieldDefinition) instanceof FieldResolver; + } + + /** + * Check if field has a resolver directive. + * + * @param FieldDefinitionNode $field + * + * @return bool + */ + public function hasFieldMiddleware($field) + { + return collect($field->directives)->map(function (DirectiveNode $directive) { + return $this->handler($directive->name->value); + })->reduce(function ($has, $handler) { + return $handler instanceof FieldMiddleware ? true : $has; + }, false); + } + + /** + * Get handler for field. + * + * @param FieldDefinitionNode $field + * + * @throws DirectiveException + * + * @return FieldResolver|null + */ + public function fieldResolver($field) + { + $resolvers = $this->directives($field)->filter(function ($directive) { + return $directive instanceof FieldResolver; + }); + + if ($resolvers->count() > 1) { + throw new DirectiveException(sprintf( + 'Fields can only have 1 assigned resolver directive. %s has %s resolver directives [%s]', + data_get($field, 'name.value'), + $resolvers->count(), + collect($field->directives)->map(function (DirectiveNode $directive) { + return $directive->name->value; + })->implode(', ') + )); + } + + $resolver = $resolvers->first(); + + return $resolver ? $this->hydrate($resolver, $field) : null; + } + + /** + * Get all middleware directive for a type definitions. + * + * @param Node $typeDefinition + * + * @return \Illuminate\Support\Collection + */ + public function nodeMiddleware(Node $typeDefinition) + { + return $this->directives($typeDefinition)->filter(function (Directive $directive) { + return $directive instanceof NodeMiddleware; + }); + } + + /** + * Get middleware for field. + * + * @param FieldDefinitionNode $fieldDefinition + * + * @return \Illuminate\Support\Collection + */ + public function fieldMiddleware($fieldDefinition) + { + return $this->directives($fieldDefinition)->filter(function ($handler) { + return $handler instanceof FieldMiddleware; + })->map(function (FieldMiddleware $fieldDirective) use ($fieldDefinition) { + return $this->hydrate($fieldDirective, $fieldDefinition); + }); + } + + /** + * Get middleware for field arguments. + * + * @param InputValueDefinitionNode $arg + * + * @return \Illuminate\Support\Collection + */ + public function argMiddleware(InputValueDefinitionNode $arg) + { + return $this->directives($arg)->filter(function (Directive $directive) { + return $directive instanceof ArgMiddleware; + }); + } + + /** + * @param Directive $directive + * @param FieldDefinitionNode $fieldDefinition + * + * @return Directive + */ + protected function hydrate(Directive $directive, FieldDefinitionNode $fieldDefinition) + { + return $directive instanceof BaseFieldDirective + ? $directive->hydrate($fieldDefinition) + : $directive; + } +} diff --git a/src/Schema/Directives/Args/InFilterDirective.php b/src/Schema/Directives/Args/InFilterDirective.php index 2e6fd1268a..be9f9a7efe 100644 --- a/src/Schema/Directives/Args/InFilterDirective.php +++ b/src/Schema/Directives/Args/InFilterDirective.php @@ -26,7 +26,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Args/NotEqualsFilterDirective.php b/src/Schema/Directives/Args/NotEqualsFilterDirective.php index e8730e9552..5e071bd8ba 100644 --- a/src/Schema/Directives/Args/NotEqualsFilterDirective.php +++ b/src/Schema/Directives/Args/NotEqualsFilterDirective.php @@ -26,7 +26,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Args/NotInFilterDirective.php b/src/Schema/Directives/Args/NotInFilterDirective.php index 73a85be49f..c69b5f6a3d 100644 --- a/src/Schema/Directives/Args/NotInFilterDirective.php +++ b/src/Schema/Directives/Args/NotInFilterDirective.php @@ -26,7 +26,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Args/ScoutDirective.php b/src/Schema/Directives/Args/ScoutDirective.php index 69e9a4fb7a..c91790ce46 100644 --- a/src/Schema/Directives/Args/ScoutDirective.php +++ b/src/Schema/Directives/Args/ScoutDirective.php @@ -1,6 +1,5 @@ within($within); } @@ -57,4 +56,4 @@ public function handleArgument(ArgumentValue $argument) ] ); } -} \ No newline at end of file +} diff --git a/src/Schema/Directives/Args/ValidateDirective.php b/src/Schema/Directives/Args/ValidateDirective.php index d1a63923f3..79f64e15b1 100644 --- a/src/Schema/Directives/Args/ValidateDirective.php +++ b/src/Schema/Directives/Args/ValidateDirective.php @@ -40,7 +40,8 @@ public function handleField(FieldValue $value) ); if (! $validator) { - $message = 'A `validator` argument must be supplied on the @validate field directive'; + $fieldName = $value->getFieldName(); + $message = "A `validator` argument must be supplied on the @validate directive on field {$fieldName}"; throw new DirectiveException($message); } @@ -65,7 +66,7 @@ public function handleField(FieldValue $value) * * @param ArgumentValue $value * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $value) { diff --git a/src/Schema/Directives/Args/WhereBetweenFilterDirective.php b/src/Schema/Directives/Args/WhereBetweenFilterDirective.php index 2a03dbd3fa..74734d5d61 100644 --- a/src/Schema/Directives/Args/WhereBetweenFilterDirective.php +++ b/src/Schema/Directives/Args/WhereBetweenFilterDirective.php @@ -27,7 +27,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Args/WhereFilterDirective.php b/src/Schema/Directives/Args/WhereFilterDirective.php index b05607d712..cd9fed13ee 100644 --- a/src/Schema/Directives/Args/WhereFilterDirective.php +++ b/src/Schema/Directives/Args/WhereFilterDirective.php @@ -26,7 +26,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Args/WhereNotBetweenFilterDirective.php b/src/Schema/Directives/Args/WhereNotBetweenFilterDirective.php index 7a39acb38e..04595297f3 100644 --- a/src/Schema/Directives/Args/WhereNotBetweenFilterDirective.php +++ b/src/Schema/Directives/Args/WhereNotBetweenFilterDirective.php @@ -27,7 +27,7 @@ public function name() * * @param ArgumentValue $argument * - * @return array + * @return ArgumentValue */ public function handleArgument(ArgumentValue $argument) { diff --git a/src/Schema/Directives/Fields/AuthDirective.php b/src/Schema/Directives/Fields/AuthDirective.php index 0754401581..97f59c2286 100644 --- a/src/Schema/Directives/Fields/AuthDirective.php +++ b/src/Schema/Directives/Fields/AuthDirective.php @@ -4,12 +4,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class AuthDirective implements FieldResolver +class AuthDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives; - /** * Name of the directive. * @@ -29,10 +26,7 @@ public function name() */ public function resolveField(FieldValue $value) { - $guard = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'guard' - ); + $guard = $this->associatedArgValue('name'); return $value->setResolver(function () use ($guard) { return auth($guard)->user(); diff --git a/src/Schema/Directives/Fields/BaseFieldDirective.php b/src/Schema/Directives/Fields/BaseFieldDirective.php new file mode 100644 index 0000000000..725aa6d7c5 --- /dev/null +++ b/src/Schema/Directives/Fields/BaseFieldDirective.php @@ -0,0 +1,100 @@ +fieldDefinition = $fieldDefinition; + + return $this; + } + + /** + * Get the directive definition that belongs to the current directive. + * + * @return DirectiveNode + */ + protected function associatedDirective() + { + return $this->fieldDirective($this->fieldDefinition, $this->name()); + } + + /** + * Get an argument value from the directive definition that belongs to the current directive. + * + * @param string $argName + * @param mixed|null $default + * + * @return mixed + */ + protected function associatedArgValue($argName, $default = null) + { + $directive = $this->associatedDirective($this->fieldDefinition); + + return $this->directiveArgValue($directive, $argName, $default); + } + + /** + * Add the namespace to a classname and check if it exists. + * + * @param string $baseClassName + * + * @throws DirectiveException + * + * @return string + */ + protected function namespaceClassName($baseClassName) + { + $className = $this->associatedNamespace() . '\\' . $baseClassName; + + if (!class_exists($className)) { + $directiveName = $this->name(); + throw new DirectiveException("No class '$className' was found for directive '$directiveName'"); + } + + return $className; + } + + /** + * Get the namespace for this field, returns an empty string if its not set. + * + * @return string + */ + protected function associatedNamespace() + { + $namespaceDirective = $this->fieldDirective( + $this->fieldDefinition, + (new NamespaceDirective)->name() + ); + + return $namespaceDirective + // Look if a namespace for the current field is set, if not default to an empty string + ? $this->directiveArgValue($namespaceDirective, $this->name(), '') + // Default to an empty namespace if the namespace directive does not exist + : ''; + } +} diff --git a/src/Schema/Directives/Fields/BelongsToDirective.php b/src/Schema/Directives/Fields/BelongsToDirective.php index 5f10d053e1..0dd7e9f27f 100644 --- a/src/Schema/Directives/Fields/BelongsToDirective.php +++ b/src/Schema/Directives/Fields/BelongsToDirective.php @@ -5,12 +5,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\DataLoader\Loaders\BelongsToLoader; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class BelongsToDirective implements FieldResolver +class BelongsToDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives; - /** * Name of the directive. * @@ -30,11 +27,7 @@ public function name() */ public function resolveField(FieldValue $value) { - $relation = $this->directiveArgValue( - $this->fieldDirective($value->getField(), 'belongsTo'), - 'relation', - $value->getField()->name->value - ); + $relation = $this->associatedArgValue('relation', $value->getFieldName()); return $value->setResolver(function ($root, array $args, $context = null, $info = null) use ($relation) { return graphql()->batch(BelongsToLoader::class, $root->getKey(), [ diff --git a/src/Schema/Directives/Fields/CanDirective.php b/src/Schema/Directives/Fields/CanDirective.php index 40228e6de6..16a660f100 100644 --- a/src/Schema/Directives/Fields/CanDirective.php +++ b/src/Schema/Directives/Fields/CanDirective.php @@ -5,12 +5,9 @@ use GraphQL\Error\Error; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class CanDirective implements FieldMiddleware +class CanDirective extends BaseFieldDirective implements FieldMiddleware { - use HandlesDirectives; - /** * Name of the directive. * @@ -30,16 +27,8 @@ public function name() */ public function handleField(FieldValue $value) { - $policies = $this->directiveArgValue( - $this->fieldDirective($value->getField(), 'can'), - 'if' - ); - - $model = $this->directiveArgValue( - $this->fieldDirective($value->getField(), 'can'), - 'model' - ); - + $policies = $this->associatedArgValue('if'); + $model = $this->associatedArgValue('model'); $resolver = $value->getResolver(); return $value->setResolver( @@ -48,14 +37,14 @@ function () use ($policies, $resolver, $model) { $model = $model ?: get_class($args[0]); $can = collect($policies)->reduce(function ($allowed, $policy) use ($model) { - if (! app('auth')->user()->can($policy, $model)) { + if (!app('auth')->user()->can($policy, $model)) { return false; } return $allowed; }, true); - if (! $can) { + if (!$can) { throw new Error('Not authorized to access resource'); } diff --git a/src/Schema/Directives/Fields/ComplexityDirective.php b/src/Schema/Directives/Fields/ComplexityDirective.php index 4a31855126..8f598cc855 100644 --- a/src/Schema/Directives/Fields/ComplexityDirective.php +++ b/src/Schema/Directives/Fields/ComplexityDirective.php @@ -4,12 +4,10 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; -use Nuwave\Lighthouse\Support\Traits\CanParseResolvers; +use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -class ComplexityDirective implements FieldMiddleware +class ComplexityDirective extends BaseFieldDirective implements FieldMiddleware { - use CanParseResolvers; - /** * Name of the directive. * @@ -29,20 +27,25 @@ public function name() */ public function handleField(FieldValue $value) { - $directive = $this->fieldDirective($value->getField(), $this->name()); + $baseClassName = $this->associatedArgValue('class') ?? str_before($this->associatedArgValue('resolver'), '@'); - if ($resolver = $this->getResolver($value, $directive, false)) { - $method = $this->getResolverMethod($directive); + if (empty($baseClassName)) { + return $value->setComplexity(function ($childrenComplexity, $args) { + $complexity = array_get($args, 'first', array_get($args, 'count', 1)); - return $value->setComplexity(function () use ($resolver, $method) { - return call_user_func_array([app($resolver), $method], func_get_args()); + return $childrenComplexity * $complexity; }); } - return $value->setComplexity(function ($childrenComplexity, $args) { - $complexity = array_get($args, 'first', array_get($args, 'count', 1)); + $resolverClass = $this->namespaceClassName($baseClassName); + $resolverMethod = $this->associatedArgValue('method') ?? str_after($this->associatedArgValue('resolver'), '@'); + + if (!method_exists($resolverClass, $resolverMethod)) { + throw new DirectiveException("Method '{$resolverMethod}' does not exist on class '{$resolverClass}'"); + } - return $childrenComplexity * $complexity; + return $value->setComplexity(function () use ($resolverClass, $resolverMethod) { + return call_user_func_array([app($resolverClass), $resolverMethod], func_get_args()); }); } } diff --git a/src/Schema/Directives/Fields/CreateDirective.php b/src/Schema/Directives/Fields/CreateDirective.php index 3b38db96ca..e7b5a33090 100644 --- a/src/Schema/Directives/Fields/CreateDirective.php +++ b/src/Schema/Directives/Fields/CreateDirective.php @@ -5,12 +5,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class CreateDirective implements FieldResolver +class CreateDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives; - /** * Name of the directive. * @@ -31,12 +28,9 @@ public function name() public function resolveField(FieldValue $value) { // TODO: create a model registry so we can auto-resolve this. - $model = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'model' - ); + $model = $this->associatedArgValue('model'); - if (! $model) { + if (!$model) { throw new DirectiveException(sprintf( 'The `create` directive on %s [%s] must have a `model` argument', $value->getNodeName(), diff --git a/src/Schema/Directives/Fields/DeleteDirective.php b/src/Schema/Directives/Fields/DeleteDirective.php index 2062dcb84f..5402ce727a 100644 --- a/src/Schema/Directives/Fields/DeleteDirective.php +++ b/src/Schema/Directives/Fields/DeleteDirective.php @@ -7,12 +7,11 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; -class DeleteDirective implements FieldResolver +class DeleteDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives, HandlesGlobalId; + use HandlesGlobalId; /** * Name of the directive. @@ -34,18 +33,10 @@ public function name() public function resolveField(FieldValue $value) { $idArg = $this->getIDField($value); - $class = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'model' - ); + $class = $this->associatedArgValue('model'); + $globalId = $this->associatedArgValue('globalId', false); - $globalId = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'globalId', - false - ); - - if (! $class) { + if (!$class) { throw new DirectiveException(sprintf( 'The `delete` directive on %s [%s] must have a `model` argument', $value->getNodeName(), @@ -53,7 +44,7 @@ public function resolveField(FieldValue $value) )); } - if (! $idArg) { + if (!$idArg) { new DirectiveException(sprintf( 'The `delete` requires that you have an `ID` field on %s', $value->getNodeName() diff --git a/src/Schema/Directives/Fields/EventDirective.php b/src/Schema/Directives/Fields/EventDirective.php index a457552d5b..780a77ac11 100644 --- a/src/Schema/Directives/Fields/EventDirective.php +++ b/src/Schema/Directives/Fields/EventDirective.php @@ -3,15 +3,11 @@ namespace Nuwave\Lighthouse\Schema\Directives\Fields; use Closure; -use GraphQL\Language\AST\FieldDefinitionNode; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class EventDirective implements FieldMiddleware +class EventDirective extends BaseFieldDirective implements FieldMiddleware { - use HandlesDirectives; - /** * Name of the directive. * @@ -27,38 +23,21 @@ public function name() * * @param FieldValue $value * - * @return Closure + * @return FieldValue + * @throws \Nuwave\Lighthouse\Support\Exceptions\DirectiveException */ public function handleField(FieldValue $value) { - $event = $this->getEvent($value->getField()); + $eventBaseName = $this->associatedArgValue('fire') ?? $this->associatedArgValue('class'); + $eventClassName = $this->namespaceClassName($eventBaseName); $resolver = $value->getResolver(); - return $value->setResolver(function () use ($resolver, $event) { + return $value->setResolver(function () use ($resolver, $eventClassName) { $args = func_get_args(); $value = call_user_func_array($resolver, $args); - event(new $event($value)); + event(new $eventClassName($value)); return $value; }); } - - /** - * Get the event name. - * - * @param FieldDefinitionNode $field - * - * @return mixed - */ - protected function getEvent(FieldDefinitionNode $field) - { - return $this->directiveArgValue( - $this->fieldDirective($field, 'event'), - 'fire', - $this->directiveArgValue( - $this->fieldDirective($field, 'event'), - 'class' - ) - ); - } } diff --git a/src/Schema/Directives/Fields/FieldDirective.php b/src/Schema/Directives/Fields/FieldDirective.php index c706f01f8c..54466e097e 100644 --- a/src/Schema/Directives/Fields/FieldDirective.php +++ b/src/Schema/Directives/Fields/FieldDirective.php @@ -2,16 +2,12 @@ namespace Nuwave\Lighthouse\Schema\Directives\Fields; -use GraphQL\Language\AST\DirectiveNode; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\CanParseResolvers; -class FieldDirective implements FieldResolver +class FieldDirective extends BaseFieldDirective implements FieldResolver { - use CanParseResolvers; - /** * Field resolver. * @@ -45,19 +41,26 @@ public function name() */ public function resolveField(FieldValue $value) { - $directive = $this->fieldDirective($value->getField(), $this->name()); - $resolver = $this->getResolver($value, $directive); - $method = $this->getResolverMethod($directive); - $data = $this->argValue(collect($directive->arguments)->first(function ($arg) { - return 'args' === data_get($arg, 'name.value'); - })); + $baseClassName = $this->associatedArgValue('class') ?? str_before($this->associatedArgValue('resolver'), '@'); + + if (empty($baseClassName)) { + $directiveName = $this->name(); + throw new DirectiveException("Directive '{$directiveName}' must have a `class` argument."); + } + + $resolverClass = $this->namespaceClassName($baseClassName); + $resolverMethod = $this->associatedArgValue('method') ?? str_after($this->associatedArgValue('resolver'), '@'); + + if (! method_exists($resolverClass, $resolverMethod)) { + throw new DirectiveException("Method '{$resolverMethod}' does not exist on class '{$resolverClass}'"); + } - return $value->setResolver(function ($root, array $args, $context = null, $info = null) use ($resolver, $method, $data) { - $instance = app($resolver); + $additionalData = $this->associatedArgValue('args'); + return $value->setResolver(function ($root, array $args, $context = null, $info = null) use ($resolverClass, $resolverMethod, $additionalData) { return call_user_func_array( - [$instance, $method], - [$root, array_merge($args, ['directive' => $data]), $context, $info] + [app($resolverClass), $resolverMethod], + [$root, array_merge($args, ['directive' => $additionalData]), $context, $info] ); }); } diff --git a/src/Schema/Directives/Fields/FindDirective.php b/src/Schema/Directives/Fields/FindDirective.php index 9ab3d793d2..2a6f6a1832 100644 --- a/src/Schema/Directives/Fields/FindDirective.php +++ b/src/Schema/Directives/Fields/FindDirective.php @@ -1,21 +1,19 @@ applyFilters($model::query(), $args); $query = $this->applyScopes($query, $args, $value); $total = $query->count(); - if($total > 1) { + if ($total > 1) { throw new Error('Query returned more than one result.'); } return $query->first(); }); } -} \ No newline at end of file +} diff --git a/src/Schema/Directives/Fields/FirstDirective.php b/src/Schema/Directives/Fields/FirstDirective.php index 63d68979fc..deb1002f19 100644 --- a/src/Schema/Directives/Fields/FirstDirective.php +++ b/src/Schema/Directives/Fields/FirstDirective.php @@ -1,18 +1,16 @@ first(); }); } -} \ No newline at end of file +} diff --git a/src/Schema/Directives/Fields/GlobalIdDirective.php b/src/Schema/Directives/Fields/GlobalIdDirective.php index 7f28076e54..41e16aec61 100644 --- a/src/Schema/Directives/Fields/GlobalIdDirective.php +++ b/src/Schema/Directives/Fields/GlobalIdDirective.php @@ -4,12 +4,11 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; -class GlobalIdDirective implements FieldMiddleware +class GlobalIdDirective extends BaseFieldDirective implements FieldMiddleware { - use HandlesDirectives, HandlesGlobalId; + use HandlesGlobalId; /** * Name of the directive. @@ -32,19 +31,15 @@ public function handleField(FieldValue $value) { $type = $value->getNodeName(); $resolver = $value->getResolver(); - $process = $this->directiveArgValue( - $this->fieldDirective($value->getField(), 'globalId'), - 'process', - 'encode' - ); + $process = $this->associatedArgValue('process', 'encode'); return $value->setResolver(function () use ($resolver, $process, $type) { $args = func_get_args(); $value = call_user_func_array($resolver, $args); return 'encode' === $process - ? $this->encodeGlobalId($type, $value) - : $this->decodeRelayId($value); + ? $this->encodeGlobalId($type, $value) + : $this->decodeRelayId($value); }); } } diff --git a/src/Schema/Directives/Fields/HasManyDirective.php b/src/Schema/Directives/Fields/HasManyDirective.php index 580d43c585..1d8f032c8e 100644 --- a/src/Schema/Directives/Fields/HasManyDirective.php +++ b/src/Schema/Directives/Fields/HasManyDirective.php @@ -3,18 +3,17 @@ namespace Nuwave\Lighthouse\Schema\Directives\Fields; use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Type\Definition\ResolveInfo; +use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\DataLoader\Loaders\HasManyLoader; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\CreatesPaginators; -use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; -class HasManyDirective implements FieldResolver +class HasManyDirective extends PaginationManipulator implements FieldResolver, FieldManipulator { - use CreatesPaginators, HandlesGlobalId; - /** * Name of the directive. * @@ -26,148 +25,72 @@ public function name() } /** - * Resolve the field directive. + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parentType + * @param DocumentAST $current + * @param DocumentAST $original * - * @param FieldValue $value + * @throws DirectiveException * - * @return FieldValue + * @return DocumentAST */ - public function resolveField(FieldValue $value) + public function manipulateSchema(FieldDefinitionNode $fieldDefinition, ObjectTypeDefinitionNode $parentType, DocumentAST $current, DocumentAST $original) { - $relation = $this->getRelationshipName($value->getField()); - $resolver = $this->getResolver($value->getField()); - - if (! in_array($resolver, ['default', 'paginator', 'relay', 'connection'])) { - throw new DirectiveException(sprintf( - '[%s] is not a valid `type` on `hasMany` directive [`paginator`, `relay`, `default`].', - $resolver - )); - } + $paginationType = $this->getResolverType(); - switch ($resolver) { - case 'paginator': - return $value->setResolver( - $this->paginatorTypeResolver($relation, $value) - ); - case 'connection': - case 'relay': - return $value->setResolver( - $this->connectionTypeResolver($relation, $value) - ); + switch ($paginationType) { + case self::PAGINATION_TYPE_PAGINATOR: + return $this->registerPaginator($fieldDefinition, $parentType, $current, $original); + case self::PAGINATION_TYPE_CONNECTION: + return $this->registerConnection($fieldDefinition, $parentType, $current, $original); default: - return $value->setResolver( - $this->defaultResolver($relation, $value) - ); + // Leave the field as-is when no pagination is requested + return $current; } } /** - * Get has many relationship name. - * - * @param FieldDefinitionNode $field - * - * @return string - */ - protected function getRelationshipName(FieldDefinitionNode $field) - { - return $this->directiveArgValue( - $this->fieldDirective($field, $this->name()), - 'relation', - $field->name->value - ); - } - - /** - * Get resolver type. - * - * @param FieldDefinitionNode $field - * - * @return string - */ - protected function getResolver(FieldDefinitionNode $field) - { - return $this->directiveArgValue( - $this->fieldDirective($field, $this->name()), - 'type', - 'default' - ); - } - - /** - * Get connection type. + * Resolve the field directive. * - * @param string $relation * @param FieldValue $value * - * @return \Closure + * @return FieldValue */ - protected function connectionTypeResolver($relation, FieldValue $value) + public function resolveField(FieldValue $value) { - $this->registerConnection($value); - $scopes = $this->getScopes($value); + $relation = $this->associatedArgValue('relation', $value->getFieldName()); + $type = $this->getResolverType(); + $scopes = $this->associatedArgValue('scopes', []); - return function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes) { + return $value->setResolver(function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes, $type) { return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( compact('relation', 'parent', 'args', 'scopes'), - ['type' => 'relay'] + ['type' => $type] ), HasManyLoader::key($parent, $relation, $info)); - }; + }); } /** - * Get paginator type resolver. + * @throws DirectiveException * - * @param string $relation - * @param FieldValue $value - * - * @return \Closure + * @return string */ - protected function paginatorTypeResolver($relation, FieldValue $value) + protected function getResolverType() { - $this->registerPaginator($value); - $scopes = $this->getScopes($value); + $paginationType = $this->associatedArgValue('type', 'default'); - return function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes) { - return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( - compact('relation', 'parent', 'args', 'scopes'), - ['type' => 'paginator'] - ), HasManyLoader::key($parent, $relation, $info)); - }; - } + if ('default' === $paginationType) { + return $paginationType; + } - /** - * Use default resolver for field. - * - * @param FieldValue $value - * @param string $relation - * - * @return \Closure - */ - protected function defaultResolver($relation, FieldValue $value) - { - $scopes = $this->getScopes($value); + $paginationType = $this->convertAliasToPaginationType($paginationType); - return function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes) { - return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( - compact('relation', 'parent', 'args', 'scopes'), - ['type' => 'default'] - ), HasManyLoader::key($parent, $relation, $info)); - }; - } + if (!$this->isValidPaginationType($paginationType)) { + $fieldName = $this->fieldDefinition->name->value; + $directiveName = self::name(); + throw new DirectiveException("'$paginationType' is not a valid pagination type. Field: '$fieldName', Directive: '$directiveName'"); + } - /** - * Get scope(s) to run on connection. - * - * @param FieldValue $value - * - * @return array - */ - protected function getScopes(FieldValue $value) - { - return $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'scopes', - [] - ); + return $paginationType; } } diff --git a/src/Schema/Directives/Fields/InjectDirective.php b/src/Schema/Directives/Fields/InjectDirective.php index a95e975839..d3db208927 100644 --- a/src/Schema/Directives/Fields/InjectDirective.php +++ b/src/Schema/Directives/Fields/InjectDirective.php @@ -5,12 +5,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class InjectDirective implements FieldMiddleware +class InjectDirective extends BaseFieldDirective implements FieldMiddleware { - use HandlesDirectives; - /** * Name of the directive. * @@ -31,17 +28,10 @@ public function name() public function handleField(FieldValue $value) { $resolver = $value->getResolver(); - $attr = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'context' - ); - - $name = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'name' - ); + $attr = $this->associatedArgValue('context'); + $name = $this->associatedArgValue('name'); - if (! $attr) { + if (!$attr) { throw new DirectiveException(sprintf( 'The `inject` directive on %s [%s] must have a `context` argument', $value->getNodeName(), @@ -49,7 +39,7 @@ public function handleField(FieldValue $value) )); } - if (! $name) { + if (!$name) { throw new DirectiveException(sprintf( 'The `inject` directive on %s [%s] must have a `name` argument', $value->getNodeName(), diff --git a/src/Schema/Directives/Fields/MethodDirective.php b/src/Schema/Directives/Fields/MethodDirective.php index 3e4878eb48..c056eb35ac 100644 --- a/src/Schema/Directives/Fields/MethodDirective.php +++ b/src/Schema/Directives/Fields/MethodDirective.php @@ -5,12 +5,9 @@ use GraphQL\Type\Definition\ResolveInfo; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class MethodDirective implements FieldResolver +class MethodDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives; - /** * Name of the directive. * @@ -30,11 +27,7 @@ public function name() */ public function resolveField(FieldValue $value) { - $method = $this->directiveArgValue( - $this->fieldDirective($value->getField(), 'method'), - 'name', - $value->getField()->name->value - ); + $method = $this->associatedArgValue('name', $value->getFieldName()); return $value->setResolver(function ($root, array $args, $context = null, ResolveInfo $info = null) use ($method) { return call_user_func_array([$root, $method], [$args, $context, $info]); diff --git a/src/Schema/Directives/Fields/MiddlewareDirective.php b/src/Schema/Directives/Fields/MiddlewareDirective.php index 6a0876792f..d7cfefe72d 100644 --- a/src/Schema/Directives/Fields/MiddlewareDirective.php +++ b/src/Schema/Directives/Fields/MiddlewareDirective.php @@ -4,12 +4,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class MiddlewareDirective implements FieldMiddleware +class MiddlewareDirective extends BaseFieldDirective implements FieldMiddleware { - use HandlesDirectives; - /** * Name of the directive. * @@ -57,16 +54,13 @@ public function handleField(FieldValue $value) */ protected function getChecks(FieldValue $value) { - if (! in_array($value->getNodeName(), ['Mutation', 'Query'])) { + if (!in_array($value->getNodeName(), ['Mutation', 'Query'])) { return null; } - $checks = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'checks' - ); + $checks = $this->associatedArgValue('checks'); - if (! $checks) { + if (!$checks) { return null; } diff --git a/src/Schema/Directives/Fields/NamespaceDirective.php b/src/Schema/Directives/Fields/NamespaceDirective.php new file mode 100644 index 0000000000..91d89efe74 --- /dev/null +++ b/src/Schema/Directives/Fields/NamespaceDirective.php @@ -0,0 +1,31 @@ +getPaginationType()) { + case self::PAGINATION_TYPE_CONNECTION: + return $this->registerConnection($fieldDefinition, $parentType, $current, $original); + case self::PAGINATION_TYPE_PAGINATOR: + return $this->registerPaginator($fieldDefinition, $parentType, $current, $original); + } + } + /** * Resolve the field directive. * * @param FieldValue $value * - * @return FieldValue * @throws DirectiveException + * + * @return FieldValue */ public function resolveField(FieldValue $value) { - $type = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'type', - 'paginator' - ); + $paginationType = $this->getPaginationType(); $model = $this->getModelClass($value); - $resolver = in_array($type, ['relay', 'connection']) - ? $this->connectionTypeResolver($value, $model) - : $this->paginatorTypeResolver($value, $model); + switch ($paginationType) { + case self::PAGINATION_TYPE_CONNECTION: + return $this->connectionTypeResolver($value, $model); + case self::PAGINATION_TYPE_PAGINATOR: + return $this->paginatorTypeResolver($value, $model); + } + } - return $value->setResolver($resolver); + /** + * @return string + * @throws DirectiveException + */ + protected function getPaginationType() + { + $paginationType = $this->associatedArgValue('type', self::PAGINATION_TYPE_PAGINATOR); + + $paginationType = $this->convertAliasToPaginationType($paginationType); + if (!$this->isValidPaginationType($paginationType)) { + $fieldName = $this->fieldDefinition->name->value; + $directiveName = self::name(); + throw new DirectiveException("'$paginationType' is not a valid pagination type. Field: '$fieldName', Directive: '$directiveName'"); + } + + return $paginationType; } /** * Create a paginator resolver. * * @param FieldValue $value - * @param string $model + * @param string $model * - * @return \Closure + * @return FieldValue */ protected function paginatorTypeResolver(FieldValue $value, $model) { - $this->registerPaginator($value); - - return function ($root, array $args) use ($model, $value) { + return $value->setResolver(function ($root, array $args) use ($model, $value) { $first = data_get($args, 'count', 15); $page = data_get($args, 'page', 1); $query = $this->applyFilters($model::query(), $args); $query = $this->applyScopes($query, $args, $value); - Paginator::currentPageResolver(function() use ($page) { + Paginator::currentPageResolver(function () use ($page) { return $page; }); + return $query->paginate($first); - }; + }); } /** @@ -83,13 +120,11 @@ protected function paginatorTypeResolver(FieldValue $value, $model) * @param FieldValue $value * @param string $model * - * @return \Closure + * @return FieldValue */ protected function connectionTypeResolver(FieldValue $value, $model) { - $this->registerConnection($value); - - return function ($root, array $args) use ($model, $value) { + return $value->setResolver(function ($root, array $args) use ($model, $value) { $first = data_get($args, 'first', 15); $after = $this->decodeCursor($args); $page = $first && $after ? floor(($first + $after) / $first) : 1; @@ -97,10 +132,11 @@ protected function connectionTypeResolver(FieldValue $value, $model) $query = $this->applyFilters($model::query(), $args); $query = $this->applyScopes($query, $args, $value); - Paginator::currentPageResolver(function() use ($page) { + Paginator::currentPageResolver(function () use ($page) { return $page; }); + return $query->paginate($first); - }; + }); } } diff --git a/src/Schema/Directives/Fields/PaginationManipulator.php b/src/Schema/Directives/Fields/PaginationManipulator.php new file mode 100644 index 0000000000..8c9ecc419a --- /dev/null +++ b/src/Schema/Directives/Fields/PaginationManipulator.php @@ -0,0 +1,206 @@ +connectionTypeName($fieldDefinition, $parentType); + $connectionEdgeName = $this->connectionEdgeName($fieldDefinition, $parentType); + $connectionFieldName = addslashes(ConnectionField::class); + + $connectionType = PartialParser::objectTypeDefinition(" + type $connectionTypeName { + pageInfo: PageInfo! @field(class: \"$connectionFieldName\" method: \"pageInfoResolver\") + edges: [$connectionEdgeName] @field(class: \"$connectionFieldName\" method: \"edgeResolver\") + } + "); + + $nodeName = $this->unpackNodeToString($fieldDefinition); + $connectionEdge = PartialParser::objectTypeDefinition(" + type $connectionEdgeName { + node: $nodeName + cursor: String! + } + "); + + $connectionArguments = PartialParser::inputValueDefinitions([ + 'first: Int!', + 'after: String', + ]); + + $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $connectionArguments); + $fieldDefinition->type = Parser::parseType($connectionTypeName); + $parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]); + + $current->setDefinition($connectionType); + $current->setDefinition($connectionEdge); + $current->setDefinition($parentType); + + return $current; + } + + /** + * Register paginator w/ schema. + * + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parentType + * @param DocumentAST $current + * @param DocumentAST $original + * + * @throws \Exception + * + * @return DocumentAST + */ + protected function registerPaginator(FieldDefinitionNode $fieldDefinition, ObjectTypeDefinitionNode $parentType, DocumentAST $current, DocumentAST $original) + { + $paginatorTypeName = $this->paginatorTypeName($fieldDefinition, $parentType); + $paginatorFieldClassName = addslashes(PaginatorField::class); + $fieldTypeName = $this->unpackNodeToString($fieldDefinition); + + $paginatorType = PartialParser::objectTypeDefinition(" + type $paginatorTypeName { + paginatorInfo: PaginatorInfo! @field(class: \"$paginatorFieldClassName\" method: \"paginatorInfoResolver\") + data: [$fieldTypeName!]! @field(class: \"$paginatorFieldClassName\" method: \"dataResolver\") + } + "); + + $paginationArguments = PartialParser::inputValueDefinitions([ + 'count: Int!', + 'page: Int', + ]); + + $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $paginationArguments); + $fieldDefinition->type = Parser::parseType($paginatorTypeName); + $parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]); + + $current->setDefinition($paginatorType); + $current->setDefinition($parentType); + + return $current; + } + + /** + * Get paginator type name. + * + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parent + * + * @return string + */ + protected function paginatorTypeName(FieldDefinitionNode $fieldDefinition, ObjectTypeDefinitionNode $parent) + { + return studly_case( + $this->parentTypeName($parent) + . $this->singularFieldName($fieldDefinition) + . '_Paginator' + ); + } + + /** + * Get connection type name. + * + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parent + * + * @return string + */ + protected function connectionTypeName(FieldDefinitionNode $fieldDefinition, ObjectTypeDefinitionNode $parent) + { + return studly_case( + $this->parentTypeName($parent) + . $this->singularFieldName($fieldDefinition) + . '_Connection' + ); + } + + /** + * Get connection edge name. + * + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parent + * + * @return string + */ + protected function connectionEdgeName(FieldDefinitionNode $fieldDefinition, ObjectTypeDefinitionNode $parent) + { + return studly_case( + $this->parentTypeName($parent) + . $this->singularFieldName($fieldDefinition) + . '_Edge' + ); + } + + /** + * @param FieldDefinitionNode $fieldDefinition + * + * @return string + */ + protected function singularFieldName(FieldDefinitionNode $fieldDefinition) + { + return str_singular($fieldDefinition->name->value); + } + + /** + * @param ObjectTypeDefinitionNode $objectType + * + * @return string + */ + protected function parentTypeName(ObjectTypeDefinitionNode $objectType) + { + $name = $objectType->name->value; + + return 'Query' === $name ? '' : $name . '_'; + } +} diff --git a/src/Schema/Directives/Fields/RenameDirective.php b/src/Schema/Directives/Fields/RenameDirective.php index ef6d4a8e67..4f86edd09a 100644 --- a/src/Schema/Directives/Fields/RenameDirective.php +++ b/src/Schema/Directives/Fields/RenameDirective.php @@ -5,12 +5,9 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class RenameDirective implements FieldResolver +class RenameDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives; - /** * Name of the directive. * @@ -26,16 +23,14 @@ public function name() * * @param FieldValue $value * - * @return \Closure + * @return FieldValue + * @throws DirectiveException */ public function resolveField(FieldValue $value) { - $attribute = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'attribute' - ); + $attribute = $this->associatedArgValue('attribute'); - if (! $attribute) { + if (!$attribute) { throw new DirectiveException(sprintf( 'The [%s] directive requires an `attribute` argument.', $this->name() diff --git a/src/Schema/Directives/Fields/UpdateDirective.php b/src/Schema/Directives/Fields/UpdateDirective.php index 5066272704..1c17b9c4f7 100644 --- a/src/Schema/Directives/Fields/UpdateDirective.php +++ b/src/Schema/Directives/Fields/UpdateDirective.php @@ -7,12 +7,11 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; -class UpdateDirective implements FieldResolver +class UpdateDirective extends BaseFieldDirective implements FieldResolver { - use HandlesDirectives, HandlesGlobalId; + use HandlesGlobalId; /** * Name of the directive. @@ -34,18 +33,10 @@ public function name() public function resolveField(FieldValue $value) { $idArg = $this->getIDField($value); - $class = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'model' - ); + $class = $this->associatedArgValue('model'); + $globalId = $this->associatedArgValue('globalId', false); - $globalId = $this->directiveArgValue( - $this->fieldDirective($value->getField(), $this->name()), - 'globalId', - false - ); - - if (! $class) { + if (!$class) { throw new DirectiveException(sprintf( 'The `update` directive on %s [%s] must have a `model` argument', $value->getNodeName(), @@ -53,7 +44,7 @@ public function resolveField(FieldValue $value) )); } - if (! $idArg) { + if (!$idArg) { new DirectiveException(sprintf( 'The `update` requires that you have an `ID` field on %s', $value->getNodeName() diff --git a/src/Schema/Directives/Nodes/GroupDirective.php b/src/Schema/Directives/Nodes/GroupDirective.php index a54bbd57c6..d0b4ab30d1 100644 --- a/src/Schema/Directives/Nodes/GroupDirective.php +++ b/src/Schema/Directives/Nodes/GroupDirective.php @@ -2,12 +2,26 @@ namespace Nuwave\Lighthouse\Schema\Directives\Nodes; -use Nuwave\Lighthouse\Schema\Values\NodeValue; -use Nuwave\Lighthouse\Support\Contracts\NodeMiddleware; +use GraphQL\Language\AST\DirectiveNode; +use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use Nuwave\Lighthouse\Schema\AST\DocumentAST; +use Nuwave\Lighthouse\Schema\AST\PartialParser; +use Nuwave\Lighthouse\Schema\Directives\Fields\NamespaceDirective; +use Nuwave\Lighthouse\Support\Contracts\NodeManipulator; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; -class GroupDirective implements NodeMiddleware +/** + * Class GroupDirective. + * + * This directive is kept for compatibility reasons but is superseded by + * NamespaceDirective and MiddlewareDirective. + * + * @deprecated Will be removed in next major version + */ +class GroupDirective implements NodeManipulator { use HandlesDirectives; @@ -22,68 +36,115 @@ public function name() } /** - * Handle node value. + * @param ObjectTypeDefinitionNode $objectType + * @param DocumentAST $current + * @param DocumentAST $original * - * @param NodeValue $value + * @throws DirectiveException * - * @return NodeValue + * @return DocumentAST */ - public function handleNode(NodeValue $value) + public function manipulateSchema(ObjectTypeDefinitionNode $objectType, DocumentAST $current, DocumentAST $original) { - $this->setNamespace($value); - $this->setMiddleware($value); + $nodeName = $objectType->name->value; - return $value; + if (! in_array($nodeName, ['Query', 'Mutation'])) { + $message = "The group directive can only be placed on a Query or Mutation [$nodeName]"; + + throw new DirectiveException($message); + } + + $objectType = $this->setMiddlewareDirectiveOnFields($objectType); + $objectType = $this->setNamespaceDirectiveOnFields($objectType); + + $current->setDefinition($objectType); + + return $current; } /** - * Set namespace on node. + * @param ObjectTypeDefinitionNode $objectType * - * @param NodeValue $value [description] + * @throws \Exception + * + * @return ObjectTypeDefinitionNode */ - protected function setNamespace(NodeValue $value) + protected function setMiddlewareDirectiveOnFields(ObjectTypeDefinitionNode $objectType) { - $namespace = $this->directiveArgValue( - $this->nodeDirective($value->getNode(), $this->name()), - 'namespace' + $middlewareValues = $this->directiveArgValue( + $this->nodeDirective($objectType, self::name()), + 'middleware' ); - if ($namespace) { - $value->setNamespace($namespace); + if (! $middlewareValues) { + return $objectType; } + + $middlewareValues = '["'.implode('", "', $middlewareValues).'"]'; + $middlewareDirective = PartialParser::directive("@middleware(checks: $middlewareValues)"); + + $objectType->fields = new NodeList(collect($objectType->fields)->map(function (FieldDefinitionNode $fieldDefinition) use ($middlewareDirective) { + $fieldDefinition->directives = $fieldDefinition->directives->merge([$middlewareDirective]); + + return $fieldDefinition; + })->toArray()); + + return $objectType; } /** - * Set middleware for field. + * @param ObjectTypeDefinitionNode $objectType + * + * @throws \Exception * - * @param NodeValue $value + * @return ObjectTypeDefinitionNode */ - protected function setMiddleware(NodeValue $value) + protected function setNamespaceDirectiveOnFields(ObjectTypeDefinitionNode $objectType) { - $node = $value->getNodeName(); + $namespaceValue = $this->directiveArgValue( + $this->nodeDirective($objectType, self::name()), + 'namespace' + ); - if (! in_array($node, ['Query', 'Mutation'])) { - $message = 'Middleware can only be placed on a Query or Mutation ['.$node.']'; + if (! $namespaceValue) { + return $objectType; + } - throw new DirectiveException($message); + if (! is_string($namespaceValue)) { + throw new DirectiveException('The value of the namespace directive on has to be a string'); } - $middleware = $this->directiveArgValue( - $this->nodeDirective($value->getNode(), $this->name()), - 'middleware' - ); + $namespaceValue = addslashes($namespaceValue); - $container = graphql()->middleware(); - $middleware = is_string($middleware) ? [$middleware] : $middleware; + $objectType->fields = new NodeList(collect($objectType->fields)->map(function (FieldDefinitionNode $fieldDefinition) use ($namespaceValue) { + $previousNamespaces = $this->fieldDirective($fieldDefinition, (new NamespaceDirective)->name()); - if (empty($middleware)) { - return; - } + $previousNamespaces = $previousNamespaces + ? $this->mergeNamespaceOnExistingDirective($namespaceValue, $previousNamespaces) + : PartialParser::directive("@namespace(field: \"$namespaceValue\", complexity: \"$namespaceValue\")"); + $fieldDefinition->directives = $fieldDefinition->directives->merge([$previousNamespaces]); - foreach ($value->getNodeFields() as $field) { - 'Query' == $node - ? $container->registerQuery($field->name->value, $middleware) - : $container->registerMutation($field->name->value, $middleware); - } + return $fieldDefinition; + })->toArray()); + + return $objectType; + } + + /** + * @param string $namespaceValue + * @param DirectiveNode $directive + * + * @return DirectiveNode + */ + protected function mergeNamespaceOnExistingDirective($namespaceValue, DirectiveNode $directive) + { + $namespaces = PartialParser::arguments([ + "field: \"$namespaceValue\"", + "complexity: \"$namespaceValue\"", + ]); + + $directive->arguments = $directive->arguments->merge($namespaces); + + return $directive; } } diff --git a/src/Schema/Factories/ArgumentFactory.php b/src/Schema/Factories/ArgumentFactory.php index 4aa3450070..5228ae4238 100644 --- a/src/Schema/Factories/ArgumentFactory.php +++ b/src/Schema/Factories/ArgumentFactory.php @@ -4,11 +4,12 @@ use Nuwave\Lighthouse\Schema\Resolvers\NodeResolver; use Nuwave\Lighthouse\Schema\Values\ArgumentValue; +use Nuwave\Lighthouse\Support\Contracts\ArgMiddleware; class ArgumentFactory { /** - * Convert field definition to type. + * Convert argument definition to type. * * @param ArgumentValue $value * @@ -31,7 +32,7 @@ public function handle(ArgumentValue $value) protected function applyMiddleware(ArgumentValue $value) { return directives()->argMiddleware($value->getArg()) - ->reduce(function (ArgumentValue $value, $middleware) { + ->reduce(function (ArgumentValue $value, ArgMiddleware $middleware) { return $middleware->handleArgument( $value->setMiddlewareDirective($middleware->name()) ); diff --git a/src/Schema/Factories/DirectiveFactory.php b/src/Schema/Factories/DirectiveFactory.php deleted file mode 100644 index db4d1f5713..0000000000 --- a/src/Schema/Factories/DirectiveFactory.php +++ /dev/null @@ -1,256 +0,0 @@ -directives = collect(); - } - - /** - * Register all of the commands in the given directory. - * - * https://github.com/laravel/framework/blob/5.5/src/Illuminate/Foundation/Console/Kernel.php#L190-L224 - * - * @param array|string $paths - * @param string|null $namespace - */ - public function load($paths, $namespace = null) - { - $paths = array_unique(is_array($paths) ? $paths : (array) $paths); - $paths = array_map(function ($path) { - return realpath($path); - }, array_filter($paths, function ($path) { - return is_dir($path); - })); - - if (empty($paths)) { - return; - } - - $namespace = $namespace ?: app()->getNamespace(); - $path = starts_with($namespace, 'Nuwave\\Lighthouse') - ? realpath(__DIR__.'/../../') - : app_path(); - - foreach ((new Finder())->in($paths)->files() as $directive) { - $directive = $namespace.str_replace( - ['/', '.php'], - ['\\', ''], - str_after($directive->getPathname(), $path.DIRECTORY_SEPARATOR) - ); - - if (! (new \ReflectionClass($directive))->isAbstract()) { - $this->register($directive); - } - } - } - - /** - * Register a new directive handler. - * - * @param string $handler - */ - public function register($handler) - { - $directive = app($handler); - - $this->directives->put($directive->name(), $directive); - } - - /** - * Get instance of handler for directive. - * - * @param string $name - * - * @return mixed - */ - public function handler($name) - { - $handler = $this->directives->get($name); - - if (! $handler) { - throw new DirectiveException("No directive has been registered for [{$name}]"); - } - - return $handler; - } - - /** - * Check if field has a resolver directive. - * - * @param Node $node - * - * @return bool - */ - public function hasNodeResolver(Node $node) - { - return collect(data_get($node, 'directives', []))->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->reduce(function ($has, $handler) { - return $handler instanceof NodeResolver ? true : $has; - }, false); - } - - /** - * Get handler for node. - * - * @param Node $node - * - * @return mixed - */ - public function forNode(Node $node) - { - $resolvers = collect(data_get($node, 'directives', []))->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->filter(function ($handler) { - return $handler instanceof NodeResolver; - }); - - if ($resolvers->count() > 1) { - throw new DirectiveException(sprintf( - 'Nodes can only have 1 assigned directive. %s has %s directives [%s]', - data_get($node, 'name.value'), - count($directives), - collect($directives)->map(function ($directive) { - return $directive->name->value; - })->implode(', ') - )); - } - - return $resolvers->first(); - } - - /** - * Get middleware for field. - * - * @param Node $node - * - * @return \Illuminate\Support\Collection - */ - public function nodeMiddleware(Node $node) - { - return collect(data_get($node, 'directives', []))->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->filter(function ($handler) { - return $handler instanceof NodeMiddleware; - }); - } - - /** - * Check if field has a resolver directive. - * - * @param FieldDefinitionNode $field - * - * @return bool - */ - public function hasResolver($field) - { - return collect($field->directives)->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->reduce(function ($has, $handler) { - return $handler instanceof FieldResolver ? true : $has; - }, false); - } - - /** - * Get handler for field. - * - * @param FieldDefinitionNode $field - * - * @return mixed - */ - public function fieldResolver($field) - { - $resolvers = collect($field->directives)->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->filter(function ($handler) { - return $handler instanceof FieldResolver; - }); - - if ($resolvers->count() > 1) { - throw new DirectiveException(sprintf( - 'Fields can only have 1 assigned resolver directive. %s has %s resolver directives [%s]', - data_get($field, 'name.value'), - $resolvers->count(), - collect($field->directives)->map(function (DirectiveNode $directive) { - return $directive->name->value; - })->implode(', ') - )); - } - - return $resolvers->first(); - } - - /** - * Check if field has a resolver directive. - * - * @param FieldDefinitionNode $field - * - * @return bool - */ - public function hasFieldMiddleware($field) - { - return collect($field->directives)->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->reduce(function ($has, $handler) { - return $handler instanceof FieldMiddleware ? true : $has; - }, false); - } - - /** - * Get middleware for field. - * - * @param FieldDefinitionNode $field - * - * @return \Illuminate\Support\Collection - */ - public function fieldMiddleware($field) - { - return collect($field->directives)->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->filter(function ($handler) { - return $handler instanceof FieldMiddleware; - }); - } - - /** - * Get middleware for field arguments. - * - * @param InputValueDefinitionNode $arg - * - * @return \Illuminate\Support\Collection - */ - public function argMiddleware(InputValueDefinitionNode $arg) - { - return collect($arg->directives)->map(function (DirectiveNode $directive) { - return $this->handler($directive->name->value); - })->filter(function ($handler) { - return $handler instanceof ArgMiddleware; - }); - } -} diff --git a/src/Schema/Factories/NodeFactory.php b/src/Schema/Factories/NodeFactory.php index cacdb20ee1..9d6f166fa9 100644 --- a/src/Schema/Factories/NodeFactory.php +++ b/src/Schema/Factories/NodeFactory.php @@ -103,7 +103,7 @@ protected function transform(NodeValue $value) * * @param NodeValue $value * - * @return \GraphQL\Type\Definition\EnumType + * @return NodeValue */ public function enum(NodeValue $value) { @@ -132,7 +132,7 @@ public function enum(NodeValue $value) * * @param NodeValue $value * - * @return \GraphQL\Type\Definition\ScalarType + * @return NodeValue */ public function scalar(NodeValue $value) { diff --git a/src/Schema/MiddlewareManager.php b/src/Schema/MiddlewareManager.php index d8feac8885..f06abf632a 100644 --- a/src/Schema/MiddlewareManager.php +++ b/src/Schema/MiddlewareManager.php @@ -2,15 +2,12 @@ namespace Nuwave\Lighthouse\Schema; -use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Utils\AST; -use Nuwave\Lighthouse\Support\Traits\CanParseTypes; +use Nuwave\Lighthouse\Schema\AST\DocumentAST; class MiddlewareManager { - use CanParseTypes; - /** * Registered query middleware. * @@ -34,23 +31,19 @@ class MiddlewareManager */ public function forRequest($request) { - $definitions = collect($this->parseSchema($request)->definitions); - $fragments = $definitions->filter(function ($def) { - return $def instanceof FragmentDefinitionNode; - }); - - return collect($this->parseSchema($request)->definitions) - ->filter(function ($def) { - return $def instanceof OperationDefinitionNode; - })->map(function (OperationDefinitionNode $node) use ($fragments) { + $document = DocumentAST::fromSource($request); + $fragments = $document->fragments(); + + return $document->operations() + ->map(function (OperationDefinitionNode $node) use ($fragments) { $definition = AST::toArray($node); $operation = array_get($definition, 'operation'); $fields = array_map(function ($selection) use ($fragments) { $field = array_get($selection, 'name.value'); - if ('FragmentSpread' == array_get($selection, 'kind')) { + if ('FragmentSpread' === array_get($selection, 'kind')) { $fragment = $fragments->first(function ($def) use ($field) { - return data_get($def, 'name.value') == $field; + return data_get($def, 'name.value') === $field; }); return array_pluck( @@ -73,9 +66,9 @@ public function forRequest($request) * Register query middleware. * * @param string $name - * @param array $middleware + * @param array $middleware * - * @return array + * @return void */ public function registerQuery($name, array $middleware) { @@ -86,9 +79,9 @@ public function registerQuery($name, array $middleware) * Register mutation middleware. * * @param string $name - * @param array $middleware + * @param array $middleware * - * @return array + * @return void */ public function registerMutation($name, array $middleware) { @@ -148,7 +141,7 @@ public function mutation($name) * @param OperationDefinitionNode $node * @param string * - * @return array + * @return void */ protected function byNode(OperationDefinitionNode $node, $operation) { diff --git a/src/Schema/NodeContainer.php b/src/Schema/NodeContainer.php index 6a062ee27d..659163b674 100644 --- a/src/Schema/NodeContainer.php +++ b/src/Schema/NodeContainer.php @@ -52,7 +52,7 @@ public function node($type, Closure $resolver, Closure $resolveType) * @param string $type * @param string $model * - * @return \Illuminate\Database\Eloquent\Model + * @return void */ public function model($type, $model) { diff --git a/src/Schema/Resolvers/AbstractResolver.php b/src/Schema/Resolvers/AbstractResolver.php index d04a71011a..90c30a2879 100644 --- a/src/Schema/Resolvers/AbstractResolver.php +++ b/src/Schema/Resolvers/AbstractResolver.php @@ -59,7 +59,7 @@ protected function hasDirective($node, $name) { return collect($node->directives) ->reduce(function ($match, DirectiveNode $directive) use ($name) { - return $match ?: $directive->name->value == $name ? true : false; + return $match ?: $directive->name->value === $name ? true : false; }, false); } @@ -106,7 +106,7 @@ protected function fieldDirective($node, $name, $default = null) { return collect($node->directives) ->first(function (DirectiveNode $directive) use ($name) { - return $directive->name->value == $name; + return $directive->name->value === $name; }, $default); } @@ -123,7 +123,7 @@ protected function directiveArgValue(DirectiveNode $node, $key, $default = null) { $argument = collect($node->arguments) ->first(function (ArgumentNode $arg) use ($key) { - return $arg->name->value == $key; + return $arg->name->value === $key; }); return $argument ? $argument->value->value : $default; diff --git a/src/Schema/Resolvers/EnumResolver.php b/src/Schema/Resolvers/EnumResolver.php index c6832766b1..7decc1a00c 100644 --- a/src/Schema/Resolvers/EnumResolver.php +++ b/src/Schema/Resolvers/EnumResolver.php @@ -23,7 +23,7 @@ class EnumResolver extends AbstractResolver public function generate() { $config = [ - 'name' => $this->getName(), + 'name' => $this->getName(), 'values' => $this->getValues(), ]; @@ -74,14 +74,14 @@ protected function getEnumValueKey(EnumValueDefinitionNode $node) */ protected function parseEnumNode(EnumValueDefinitionNode $node) { - $directive = $this->getDirective($node, "enum"); + $directive = $this->getDirective($node, 'enum'); if (! $directive) { return []; } return [ - 'value' => $this->directiveArgValue($directive, 'value'), + 'value' => $this->directiveArgValue($directive, 'value'), 'description' => $this->safeDescription($node->description), ]; } diff --git a/src/Schema/Resolvers/FieldTypeResolver.php b/src/Schema/Resolvers/FieldTypeResolver.php index 7e61f67610..73fcca8bee 100644 --- a/src/Schema/Resolvers/FieldTypeResolver.php +++ b/src/Schema/Resolvers/FieldTypeResolver.php @@ -59,12 +59,12 @@ public static function unpack($type) */ public function resolveNodeType($node, array $wrappers = []) { - if ('NonNullType' == $node->kind) { + if ('NonNullType' === $node->kind) { return $this->resolveNodeType( $node->type, array_merge($wrappers, ['NonNullType']) ); - } elseif ('ListType' == $node->kind) { + } elseif ('ListType' === $node->kind) { return $this->resolveNodeType( $node->type, array_merge($wrappers, ['ListType']) @@ -74,9 +74,9 @@ public function resolveNodeType($node, array $wrappers = []) return collect($wrappers) ->reverse() ->reduce(function ($type, $kind) { - if ('NonNullType' == $kind) { + if ('NonNullType' === $kind) { return Type::nonNull($type); - } elseif ('ListType' == $kind) { + } elseif ('ListType' === $kind) { return Type::listOf($type); } @@ -106,8 +106,8 @@ public function unpackNodeType($type, array $wrappers = []) ->reverse() ->reduce(function ($innerType, $wrapper) { return 'ListOfType' === $wrapper - ? Type::listOf($innerType) - : Type::nonNull($innerType); + ? Type::listOf($innerType) + : Type::nonNull($innerType); }, $type); } diff --git a/src/Schema/Resolvers/NodeResolver.php b/src/Schema/Resolvers/NodeResolver.php index 1844d5dd98..8bd8cb57ca 100644 --- a/src/Schema/Resolvers/NodeResolver.php +++ b/src/Schema/Resolvers/NodeResolver.php @@ -12,7 +12,7 @@ class NodeResolver * * @param mixed $node * - * @return mixed + * @return \GraphQL\Type\Definition\Type */ public static function resolve($node) { @@ -29,12 +29,12 @@ public static function resolve($node) */ public function fromNode($node, array $wrappers = []) { - if ('NonNullType' == $node->kind) { + if ('NonNullType' === $node->kind) { return $this->fromNode( $node->type, array_merge($wrappers, ['NonNullType']) ); - } elseif ('ListType' == $node->kind) { + } elseif ('ListType' === $node->kind) { return $this->fromNode( $node->type, array_merge($wrappers, ['ListType']) @@ -44,9 +44,9 @@ public function fromNode($node, array $wrappers = []) return collect($wrappers) ->reverse() ->reduce(function ($type, $kind) { - if ('NonNullType' == $kind) { + if ('NonNullType' === $kind) { return Type::nonNull($type); - } elseif ('ListType' == $kind) { + } elseif ('ListType' === $kind) { return Type::listOf($type); } diff --git a/src/Schema/SchemaBuilder.php b/src/Schema/SchemaBuilder.php index 9621f8a5e4..1946ec65ab 100644 --- a/src/Schema/SchemaBuilder.php +++ b/src/Schema/SchemaBuilder.php @@ -3,20 +3,18 @@ namespace Nuwave\Lighthouse\Schema; use GraphQL\Language\AST\DirectiveDefinitionNode; -use GraphQL\Language\AST\DocumentNode; -use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\TypeExtensionDefinitionNode; +use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use GraphQL\Type\SchemaConfig; +use Illuminate\Support\Collection; +use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\Factories\NodeFactory; use Nuwave\Lighthouse\Schema\Values\NodeValue; -use Nuwave\Lighthouse\Support\Traits\CanParseTypes; -use Nuwave\Lighthouse\Support\Traits\HandlesTypes; class SchemaBuilder { - use CanParseTypes, HandlesTypes; - /** * Definition weights. * @@ -29,215 +27,98 @@ class SchemaBuilder ]; /** - * Collection of schema types. + * Build an executable schema from AST. * - * @var array - */ - protected $types = []; - - /** - * Custom (client) directives. + * @param DocumentAST $documentAST * - * @var array + * @return Schema */ - protected $directives = []; - - /** - * Generate a GraphQL Schema. - * - * @param string $schema - * - * @return mixed - */ - public function build($schema) + public function build($documentAST) { - $types = $this->register($schema); - $query = $types->firstWhere('name', 'Query'); - $mutation = $types->firstWhere('name', 'Mutation'); - $subscription = $types->firstWhere('name', 'Subscription'); - - $types = $types->filter(function ($type) { - return ! in_array($type->name, ['Query', 'Mutation', 'Subscription']); - })->toArray(); - - $directives = $this->directives; - $typeLoader = function ($name) { - return $this->instance($name); - }; - - return new Schema(compact( - 'query', - 'mutation', - 'subscription', - 'types', - 'directives', - 'typeLoader' - )); - } - - /** - * Parse schema definitions. - * - * @param string $schema - * - * @return \Illuminate\Support\Collection - */ - public function register($schema) - { - $document = $schema instanceof DocumentNode - ? $schema - : $this->parseSchema($schema); - - $this->setTypes($document); - $this->extendTypes($document); - $this->setDirectives($document); - $this->injectNodeField(); - - return collect($this->types); - } + $types = $this->convertTypes($documentAST); + $this->loadRootOperationFields($types); + + $config = SchemaConfig::create() + // Always set Query since it is required + ->setQuery($types->firstWhere('name', 'Query')) + ->setTypes($types->reject($this->isOperationType())->toArray()) + ->setDirectives($this->convertDirectives($documentAST)->toArray()) + ->setTypeLoader(function ($name) { + return graphql()->types()->get($name); + }); + + // Those are optional so only add them if they are present in the schema + if ($mutation = $types->firstWhere('name', 'Mutation')) { + $config->setMutation($mutation); + } + if ($subscription = $types->firstWhere('name', 'Subscription')) { + $config->setSubscription($subscription); + } - /** - * Resolve instance by name. - * - * @param string $type - * - * @return mixed - */ - public function instance($type) - { - return collect($this->types) - ->first(function ($instance) use ($type) { - return $instance->name === $type; - }); + return new Schema($config); } /** - * Get all registered types. + * The fields for the root operations have to be loaded in advance. * - * @return array - */ - public function types() - { - return $this->types; - } - - /** - * Add type to register. + * This is because they might have to register middleware. + * Other fields can be lazy-loaded to improve performance. * - * @param ObjectType|array $type + * @param Collection $types */ - public function type($type) + protected function loadRootOperationFields(Collection $types) { - $this->types = is_array($type) - ? array_merge($this->types, $type) - : array_merge($this->types, [$type]); + $types->filter($this->isOperationType()) + ->each(function (ObjectType $type) { + // This resolves the fields which causes the fields MiddlewareDirective to run + // and thus register the (Laravel)-Middleware for the fields. + $type->getFields(); + }); } /** - * Serialize AST. + * Callback to determine whether a type is one of the three root operation types. * - * @return string + * @return \Closure */ - public function serialize() + protected function isOperationType() { - $schema = collect($this->types)->map(function ($type) { - return $this->serializeableType($type); - })->toArray(); - - return serialize($schema); + return function (Type $type) { + return in_array($type->name, ['Query', 'Mutation', 'Subscription']); + }; } /** - * Unserialize AST. + * Convert definitions to types. * - * @param string $schema + * @param DocumentAST $document * - * @return \Illuminate\Support\Collection + * @return Collection */ - public function unserialize($schema) + public function convertTypes(DocumentAST $document) { - $this->types = collect(unserialize($schema))->map(function ($type) { - return $this->unpackType($type); + return $document->typeDefinitions() + ->sortBy(function (TypeDefinitionNode $typeDefinition) { + return array_get($this->weights, get_class($typeDefinition), 9); + })->map(function (TypeDefinitionNode $typeDefinition) { + return app(NodeFactory::class)->handle(new NodeValue($typeDefinition)); + })->each(function (Type $type) { + // Register in global type registry + graphql()->types()->register($type); }); - - return collect($this->types); - } - - /** - * Set schema types. - * - * @param DocumentNode $document - */ - protected function setTypes(DocumentNode $document) - { - $types = collect($document->definitions)->reject(function ($node) { - return $node instanceof TypeExtensionDefinitionNode - || $node instanceof DirectiveDefinitionNode; - })->sortBy(function ($node) { - return array_get($this->weights, get_class($node), 9); - })->map(function (Node $node) { - return app(NodeFactory::class)->handle(new NodeValue($node)); - })->toArray(); - - // NOTE: We don't assign this above because new types may be - // declared by directives. - $this->types = array_merge($this->types, $types); } /** * Set custom client directives. * - * @param DocumentNode $document + * @param DocumentAST $document * - * @return array + * @return Collection */ - protected function setDirectives(DocumentNode $document) + protected function convertDirectives(DocumentAST $document) { - $this->directives = collect($document->definitions)->filter(function ($node) { - return $node instanceof DirectiveDefinitionNode; - })->map(function (Node $node) { - return app(NodeFactory::class)->handle(new NodeValue($node)); - })->toArray(); - } - - /** - * Extend registered types. - * - * @param DocumentNode $document - */ - protected function extendTypes(DocumentNode $document) - { - collect($document->definitions)->filter(function ($def) { - return $def instanceof TypeExtensionDefinitionNode; - })->each(function (TypeExtensionDefinitionNode $extension) { - $name = $extension->definition->name->value; - - if ($type = collect($this->types)->firstWhere('name', $name)) { - $value = new NodeValue($extension); - - app(NodeFactory::class)->handle($value->setType($type)); - } + return $document->directives()->map(function (DirectiveDefinitionNode $directive) { + return app(NodeFactory::class)->handle(new NodeValue($directive)); }); } - - /** - * Inject node field into Query. - */ - protected function injectNodeField() - { - if (is_null(config('lighthouse.global_id_field'))) { - return; - } - - if (! $query = $this->instance('Query')) { - return; - } - - $this->extendTypes($this->parseSchema(' - extend type Query { - node(id: ID!): Node - @field(resolver: "Nuwave\\\Lighthouse\\\Support\\\Http\\\GraphQL\\\Queries\\\NodeQuery@resolve") - } - ')); - } } diff --git a/src/Schema/TypeRegistry.php b/src/Schema/TypeRegistry.php new file mode 100644 index 0000000000..a8758f1da6 --- /dev/null +++ b/src/Schema/TypeRegistry.php @@ -0,0 +1,69 @@ +types = collect(); + } + + /** + * Resolve type instance by name. + * + * @param string $typeName + * + * @return Type + */ + public function instance($typeName) + { + return $this->get($typeName); + } + + /** + * Resolve type instance by name. + * + * @param string $typeName + * + * @return Type + */ + public function get($typeName) + { + return $this->types->get($typeName); + } + + /** + * Register type with registry. + * + * @param Type $type + * @deprecated in favour of register + */ + public function type(Type $type) + { + $this->register($type); + } + + /** + * Register type with registry. + * + * @param Type $type + */ + public function register(Type $type) + { + $this->types->put($type->name, $type); + } +} diff --git a/src/Schema/Types/ConnectionField.php b/src/Schema/Types/ConnectionField.php index dc9783a845..580ca2d0f7 100644 --- a/src/Schema/Types/ConnectionField.php +++ b/src/Schema/Types/ConnectionField.php @@ -1,4 +1,4 @@ -error('The `lighthouse.cache` setting must be set to a file path.'); - - return; - } - - $schema = graphql()->stitcher()->stitch( - config('lighthouse.global_id_field', '_id'), - config('lighthouse.schema.register') - ); - - graphql()->cache()->set($schema); - - $this->info('GraphQL AST successfully cached.'); - } -} diff --git a/src/Support/Contracts/ArgManipulator.php b/src/Support/Contracts/ArgManipulator.php new file mode 100644 index 0000000000..c795cf883c --- /dev/null +++ b/src/Support/Contracts/ArgManipulator.php @@ -0,0 +1,22 @@ +register('mutation', new \Nuwave\Lighthouse\Schema\Directives\Fields\MutationDirective()); diff --git a/src/Support/DataLoader/BatchLoader.php b/src/Support/DataLoader/BatchLoader.php index 1ef3e5b930..4907af1628 100644 --- a/src/Support/DataLoader/BatchLoader.php +++ b/src/Support/DataLoader/BatchLoader.php @@ -3,13 +3,15 @@ namespace Nuwave\Lighthouse\Support\DataLoader; use GraphQL\Deferred; +use GraphQL\Type\Definition\ResolveInfo; +use Illuminate\Database\Eloquent\Model; abstract class BatchLoader { /** * Keys to resolve. * - * @var \Illuminate\Support\Collection + * @var array */ protected $keys = []; @@ -23,13 +25,13 @@ abstract class BatchLoader /** * Generate key for field. * - * @param \Illuminate\Database\Eloquent\Model $root - * @param \GraphQL\Type\Definition\ResolveInfo $info - * @param string $relation + * @param Model $root + * @param ResolveInfo $info + * @param string $relation * * @return string */ - public static function key($root, $relation, $info = null) + public static function key(Model $root, $relation, ResolveInfo $info = null) { $path = ! empty(data_get($info, 'path')) ? array_last($info->path) : $relation; diff --git a/src/Support/DataLoader/Loaders/HasManyLoader.php b/src/Support/DataLoader/Loaders/HasManyLoader.php index 6a4d50e40c..dcec210f58 100644 --- a/src/Support/DataLoader/Loaders/HasManyLoader.php +++ b/src/Support/DataLoader/Loaders/HasManyLoader.php @@ -2,6 +2,7 @@ namespace Nuwave\Lighthouse\Support\DataLoader\Loaders; +use Nuwave\Lighthouse\Schema\Directives\Fields\PaginationManipulator; use Nuwave\Lighthouse\Support\Database\QueryFilter; use Nuwave\Lighthouse\Support\DataLoader\BatchLoader; use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; @@ -36,13 +37,14 @@ public function resolve() }]; switch ($type) { - case 'relay': + case PaginationManipulator::PAGINATION_TYPE_CONNECTION: + case PaginationManipulator::PAGINATION_ALIAS_RELAY: $first = data_get($args, 'first', 15); $after = $this->decodeCursor($args); $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; $parents->fetchForPage($first, $currentPage, $constraints); break; - case 'paginator': + case PaginationManipulator::PAGINATION_TYPE_PAGINATOR: $first = data_get($args, 'count', 15); $page = data_get($args, 'page', 1); $parents->fetchForPage($first, $page, $constraints); diff --git a/src/Support/Exceptions/DirectiveException.php b/src/Support/Exceptions/DirectiveException.php index 854d8d9948..ef2dc07e00 100644 --- a/src/Support/Exceptions/DirectiveException.php +++ b/src/Support/Exceptions/DirectiveException.php @@ -4,4 +4,6 @@ use Exception; -class DirectiveException extends Exception {} +class DirectiveException extends Exception +{ +} diff --git a/src/Support/Exceptions/ParseException.php b/src/Support/Exceptions/ParseException.php new file mode 100644 index 0000000000..9edae5203b --- /dev/null +++ b/src/Support/Exceptions/ParseException.php @@ -0,0 +1,9 @@ +validator ? $this->validator->messages() : []; + return $this->validator ? $this->validator->messages()->all() : []; } } diff --git a/src/Support/Traits/AttachesNodeInterface.php b/src/Support/Traits/AttachesNodeInterface.php new file mode 100644 index 0000000000..779194693e --- /dev/null +++ b/src/Support/Traits/AttachesNodeInterface.php @@ -0,0 +1,29 @@ +interfaces = array_merge($objectType->interfaces, [Parser::parseType('Node')]); + $globalIdFieldName = config('lighthouse.global_id_field', '_id'); + $globalIdFieldDefinition = PartialParser::fieldDefinition($globalIdFieldName . ': ID!'); + $objectType->fields->merge([$globalIdFieldDefinition]); + + return $documentAST->setDefinition($objectType); + } +} diff --git a/src/Support/Traits/CanParseResolvers.php b/src/Support/Traits/CanParseResolvers.php index e3c2db8f4b..2d1dc6669c 100644 --- a/src/Support/Traits/CanParseResolvers.php +++ b/src/Support/Traits/CanParseResolvers.php @@ -3,10 +3,14 @@ namespace Nuwave\Lighthouse\Support\Traits; use GraphQL\Language\AST\DirectiveNode; +use Nuwave\Lighthouse\Schema\Directives\Fields\NamespaceDirective; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; +/** + * @deprecated This trait will be removed in a future version of Lighthouse. + */ trait CanParseResolvers { use HandlesDirectives; @@ -24,8 +28,9 @@ protected function getResolver(FieldValue $value, DirectiveNode $directive, $thr { if ($resolver = $this->directiveArgValue($directive, 'resolver')) { $className = array_get(explode('@', $resolver), '0'); + $namespace = $this->associatedNamespace($value->getField()); - return $value->getNode()->getNamespace($className); + return $namespace ? $namespace . '\\' . $className : $className; } return $value->getNode()->getNamespace( @@ -33,6 +38,27 @@ protected function getResolver(FieldValue $value, DirectiveNode $directive, $thr ); } + /** + * Get the namespace for this field, returns an empty string if its not set. + * + * @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition + * + * @return string + */ + protected function associatedNamespace($fieldDefinition) + { + $namespaceDirective = $this->fieldDirective( + $fieldDefinition, + (new NamespaceDirective)->name() + ); + + return $namespaceDirective + // Look if a namespace for the current field is set, if not default to an empty string + ? $this->directiveArgValue($namespaceDirective, $this->name(), '') + // Default to an empty namespace if the namespace directive does not exist + : ''; + } + /** * Get class name for resolver. * @@ -45,7 +71,7 @@ protected function getResolverClassName(DirectiveNode $directive, $throw = true) { $class = $this->directiveArgValue($directive, 'class'); - if (! $class && $throw) { + if (!$class && $throw) { throw new DirectiveException(sprintf( 'Directive [%s] must have a `class` argument.', $directive->name->value @@ -72,7 +98,7 @@ protected function getResolverMethod(DirectiveNode $directive) $method = $this->directiveArgValue($directive, 'method'); - if (! $method) { + if (!$method) { throw new DirectiveException(sprintf( 'Directive [%s] must have a `method` argument.', $directive->name->value diff --git a/src/Support/Traits/CanParseTypes.php b/src/Support/Traits/CanParseTypes.php index 13a8ee14df..d332d52650 100644 --- a/src/Support/Traits/CanParseTypes.php +++ b/src/Support/Traits/CanParseTypes.php @@ -3,14 +3,14 @@ namespace Nuwave\Lighthouse\Support\Traits; use GraphQL\Language\AST\DocumentNode; - - use GraphQL\Language\AST\ObjectTypeDefinitionNode; - use GraphQL\Language\Parser; use Nuwave\Lighthouse\Schema\Factories\NodeFactory; use Nuwave\Lighthouse\Schema\Values\NodeValue; +/** + * @deprecated this trait will be removed in a future version of Lighthouse + */ trait CanParseTypes { /** @@ -60,6 +60,6 @@ protected function objectTypes(DocumentNode $document) */ protected function convertNode($node) { - return app(NodeFactory::class)->handle(new NodeValue($node)); + return (new NodeFactory)->handle(new NodeValue($node)); } } diff --git a/src/Support/Traits/CreatesPaginators.php b/src/Support/Traits/CreatesPaginators.php index 9efd1005c6..0c63e9fd20 100644 --- a/src/Support/Traits/CreatesPaginators.php +++ b/src/Support/Traits/CreatesPaginators.php @@ -6,7 +6,10 @@ use Nuwave\Lighthouse\Schema\Types\PaginatorField; use Nuwave\Lighthouse\Schema\Values\FieldValue; - +/** + * @deprecated Please use the AST to generate/modify the schema. + * This will be removed a future release. + */ trait CreatesPaginators { // TODO: Ugh, get rid of this... @@ -107,7 +110,7 @@ protected function paginatorTypeName(FieldValue $value) $parent = $value->getNodeName(); $child = str_singular($value->getField()->name->value); - return studly_case($parent.'_'.$child.'_Paginator'); + return studly_case($parent . '_' . $child . '_Paginator'); } /** @@ -122,7 +125,7 @@ protected function connectionTypeName(FieldValue $value) $parent = $value->getNodeName(); $child = str_singular($value->getField()->name->value); - return studly_case($parent.'_'.$child.'_Connection'); + return studly_case($parent . '_' . $child . '_Connection'); } /** @@ -137,6 +140,6 @@ protected function connectionEdgeName(FieldValue $value) $parent = $value->getNodeName(); $child = str_singular($value->getField()->name->value); - return studly_case($parent.'_'.$child.'_Edge'); + return studly_case($parent . '_' . $child . '_Edge'); } } diff --git a/src/Support/Traits/HandleQueries.php b/src/Support/Traits/HandlesQueries.php similarity index 96% rename from src/Support/Traits/HandleQueries.php rename to src/Support/Traits/HandlesQueries.php index b9ffb196ec..8c327ee8cf 100644 --- a/src/Support/Traits/HandleQueries.php +++ b/src/Support/Traits/HandlesQueries.php @@ -1,21 +1,19 @@ config['fields'])) { + if (!isset($type->config['fields'])) { return $type; } @@ -118,8 +118,8 @@ protected function serializeableType(Type $type) if (array_has($config, 'fields')) { $config['fields'] = collect($config['fields'])->mapWithKeys(function ($field, $key) { $field['type'] = $field['type'] instanceof Closure - ? new SerializableClosure($field['type']) - : $field['type']; + ? new SerializableClosure($field['type']) + : $field['type']; if (array_has($field, 'resolve') && $field['resolve'] instanceof Closure) { $field['resolve'] = new SerializableClosure($field['resolve']); @@ -154,9 +154,9 @@ protected function unpackFieldType($type, $wrappers = []) $unpackedType = is_callable($type) ? $type() : $type; return collect($wrappers)->reduce(function ($innerType, $type) { - if (ListOfType::class == $type) { + if (ListOfType::class === $type) { return Type::listOf($innerType); - } elseif (NonNull::class == $type) { + } elseif (NonNull::class === $type) { return Type::nonNull($innerType); } else { throw new \Exception("Unknown Type [{$type}]"); diff --git a/src/Support/Traits/IsRelayConnection.php b/src/Support/Traits/IsRelayConnection.php index 426bf3985e..0464928756 100644 --- a/src/Support/Traits/IsRelayConnection.php +++ b/src/Support/Traits/IsRelayConnection.php @@ -2,8 +2,6 @@ namespace Nuwave\Lighthouse\Support\Traits; - - trait IsRelayConnection { use HandlesGlobalId; @@ -12,9 +10,9 @@ trait IsRelayConnection * Paginate connection w/ query args. * * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $args + * @param array $args * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function scopeRelayConnection($query, array $args) { @@ -30,9 +28,9 @@ public function scopeRelayConnection($query, array $args) * Paginate connection w/ query args. * * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $args + * @param array $args * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function scopePaginatorConnection($query, array $args) { diff --git a/src/Support/helpers.php b/src/Support/helpers.php index d6c3be83ee..4cebd7deb1 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -28,11 +28,11 @@ function auth() /** * Get instance of schema container. * - * @return \Nuwave\Lighthouse\Schema\SchemaBuilder + * @return \Nuwave\Lighthouse\Schema\TypeRegistry */ function schema() { - return graphql()->schema(); + return graphql()->types(); } } @@ -40,7 +40,7 @@ function schema() /** * Get instance of directives container. * - * @return \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory + * @return \Nuwave\Lighthouse\Schema\DirectiveRegistry */ function directives() { diff --git a/tests/Integration/GraphQLTest.php b/tests/Integration/GraphQLTest.php index 93c435ecff..69274f50b7 100644 --- a/tests/Integration/GraphQLTest.php +++ b/tests/Integration/GraphQLTest.php @@ -104,7 +104,6 @@ public function itCanResolveQuery() $this->assertEquals($expected, $data); } - /** * @test */ @@ -122,7 +121,7 @@ public function itCanResolveQueryThroughController() } '; - $data = $this->postJson("graphql", ['query' => $query])->json(); + $data = $this->postJson('graphql', ['query' => $query])->json(); $expected = [ 'data' => [ @@ -138,13 +137,13 @@ public function itCanResolveQueryThroughController() $this->assertEquals($expected, $data); } - /** - * @test - */ - public function itCanResolveQueryThroughControllerViaGetRequest() - { - $this->be($this->user); - $query = ' + /** + * @test + */ + public function itCanResolveQueryThroughControllerViaGetRequest() + { + $this->be($this->user); + $query = ' query UserWithTasks { user { email @@ -155,21 +154,21 @@ public function itCanResolveQueryThroughControllerViaGetRequest() } '; - $uri = 'graphql?'.http_build_query(['query' => $query]); + $uri = 'graphql?' . http_build_query(['query' => $query]); - $data = $this->getJson($uri)->json(); + $data = $this->getJson($uri)->json(); - $expected = [ - 'data' => [ - 'user' => [ - 'email' => $this->user->email, - 'tasks' => $this->tasks->map(function ($task) { - return ['name' => $task->name]; - })->toArray(), - ], - ], - ]; + $expected = [ + 'data' => [ + 'user' => [ + 'email' => $this->user->email, + 'tasks' => $this->tasks->map(function ($task) { + return ['name' => $task->name]; + })->toArray(), + ], + ], + ]; - $this->assertEquals($expected, $data); - } + $this->assertEquals($expected, $data); + } } diff --git a/tests/Integration/Schema/Directives/Args/QueryFilterDirectiveTest.php b/tests/Integration/Schema/Directives/Args/QueryFilterDirectiveTest.php index 03fe674881..dddb2f313e 100644 --- a/tests/Integration/Schema/Directives/Args/QueryFilterDirectiveTest.php +++ b/tests/Integration/Schema/Directives/Args/QueryFilterDirectiveTest.php @@ -3,7 +3,6 @@ namespace Tests\Integration\Schema\Directives\Args; use Illuminate\Foundation\Testing\RefreshDatabase; -use Nuwave\Lighthouse\Schema\Directives\Args\EqFilterDirective; use Tests\DBTestCase; use Tests\Utils\Models\User; diff --git a/tests/Integration/Schema/Directives/Fields/HasManyDirectiveTest.php b/tests/Integration/Schema/Directives/Fields/HasManyDirectiveTest.php index 4e8ad24b6c..fd623baf3f 100644 --- a/tests/Integration/Schema/Directives/Fields/HasManyDirectiveTest.php +++ b/tests/Integration/Schema/Directives/Fields/HasManyDirectiveTest.php @@ -3,8 +3,6 @@ namespace Tests\Integration\Schema\Directives\Fields; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Pagination\LengthAwarePaginator; -use Nuwave\Lighthouse\Schema\Utils\SchemaStitcher; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; use Tests\DBTestCase; use Tests\Utils\Models\Task; @@ -53,6 +51,7 @@ public function itCanQueryHasManyRelationship() tasks: [Task!]! @hasMany } type Task { + id: Int foo: String } type Query { @@ -124,17 +123,15 @@ public function itCanQueryHasManyRelayConnection() */ public function itThrowsErrorWithUnknownTypeArg() { - $schema = ' + $this->expectException(DirectiveException::class); + $schema = $this->buildSchemaWithDefaultQuery(' type User { tasks(first: Int! after: Int): [Task!]! @hasMany(type:"foo") } type Task { foo: String - } - '; - - $this->expectException(DirectiveException::class); - $type = schema()->register($schema)->first(); + }'); + $type = $schema->getType('User'); $type->config['fields'](); } } diff --git a/tests/Integration/Schema/Directives/Fields/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/Fields/PaginateDirectiveTest.php index 2f713202a7..80a1ab3175 100644 --- a/tests/Integration/Schema/Directives/Fields/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/Fields/PaginateDirectiveTest.php @@ -57,10 +57,10 @@ public function itCanCreateQueryPaginatorsWithDifferentPages() { $users = factory(User::class, 10)->create(); $posts = factory(Post::class, 10)->create([ - 'user_id' => $users->first()->id + 'user_id' => $users->first()->id, ]); $comments = factory(Comment::class, 10)->create([ - 'post_id' => $posts->first()->id + 'post_id' => $posts->first()->id, ]); $schema = ' @@ -69,16 +69,16 @@ public function itCanCreateQueryPaginatorsWithDifferentPages() name: String! posts: [Post!]! @paginate(type: "paginator" model: "Post") } - + type Post { id: ID! comments: [Comment!]! @paginate(type: "paginator" model: "Comment") } - + type Comment { id: ID! } - + type Query { users: [User!]! @paginate(type: "paginator" model: "User") } diff --git a/tests/Integration/Schema/NodeTest.php b/tests/Integration/Schema/NodeTest.php index 29fddca4b8..83fcfa53a7 100644 --- a/tests/Integration/Schema/NodeTest.php +++ b/tests/Integration/Schema/NodeTest.php @@ -18,7 +18,7 @@ class NodeTest extends DBTestCase public function itCanResolveNodes() { $schema = ' - type User @node( + type User implements Node @node( resolver: "Tests\\\Integration\\\Schema\\\NodeTest@resolveNode" typeResolver: "Tests\\\Integration\\\Schema\\\NodeTest@resolveNodeType" ) { @@ -29,7 +29,7 @@ public function itCanResolveNodes() '; $globalId = $this->encodeGlobalId('User', $this->node['id']); - $result = $this->execute($schema, '{ node(id: "'.$globalId.'") { name } }', true); + $result = $this->execute($schema, '{ node(id: "'.$globalId.'") { ...on User { name } } }', true); $this->assertEquals($this->node['name'], array_get($result->data, 'node.name')); } @@ -43,14 +43,14 @@ public function itCanResolveModelsNodes() $globalId = $this->encodeGlobalId('User', $user->getKey()); $schema = ' - type User @model { + type User implements Node @model { _id: ID! name: String! } type Query {} '; - $result = $this->execute($schema, '{ node(id: "'.$globalId.'") { name } }', true); + $result = $this->execute($schema, '{ node(id: "'.$globalId.'") { ...on User { name } } }', true); $this->assertEquals($user->name, array_get($result->data, 'node.name')); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 52f785cf00..3903316b50 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,11 @@ namespace Tests; -use GraphQL\Executor\Executor; +use GraphQL\GraphQL; use GraphQL\Language\Parser; use Laravel\Scout\ScoutServiceProvider; -use Nuwave\Lighthouse\Schema\Values\ArgumentValue; -use Nuwave\Lighthouse\Schema\Values\FieldValue; -use Nuwave\Lighthouse\Schema\Values\NodeValue; +use Nuwave\Lighthouse\Schema\AST\ASTBuilder; +use Nuwave\Lighthouse\Schema\SchemaBuilder; use Orchestra\Testbench\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -102,30 +101,35 @@ protected function parseSchema($schema = 'schema.graphql') * * @return \GraphQL\Language\AST\DocumentNode */ - protected function parse(string $schema) + protected function parse($schema) { return Parser::parse($schema); } - + /** * Execute query/mutation. * * @param string $schema * @param string $query - * @param string $lighthouse - * @param array $variables + * @param bool $lighthouse + * @param array $variables * * @return \GraphQL\Executor\ExecutionResult */ protected function execute($schema, $query, $lighthouse = false, $variables = []) { if ($lighthouse) { - $node = file_get_contents(realpath(__DIR__.'/../assets/node.graphql')); - $lighthouse = file_get_contents(realpath(__DIR__.'/../assets/schema.graphql')); - $schema = $node."\n".$lighthouse."\n".$schema; + $addDefaultSchema = file_get_contents(realpath(__DIR__.'/../assets/schema.graphql')); + $schema = $addDefaultSchema."\n".$schema; } - return Executor::execute(schema()->build($schema), $this->parse($query)); + return GraphQL::executeQuery( + $this->buildSchemaFromString($schema), + $query, + null, + null, + $variables + ); } /** @@ -150,45 +154,29 @@ protected function store($fileName, $contents) } /** - * Get a node's field. - * - * @param string $schema - * @param int $index - * @param string|null $name + * @param string $schema * - * @return FieldValue + * @return \GraphQL\Type\Schema */ - protected function getNodeField($schema, $index = 0, $field = null) + protected function buildSchemaFromString($schema) { - $document = $this->parse($schema); - $node = new NodeValue($document->definitions[$index]); - - if (is_null($field)) { - return new FieldValue($node, array_get($node->getNodeFields(), '0')); - } - - return collect($node->getNodeFields())->filter(function ($nodeField) use ($field) { - return $nodeField->name->value === $field; - })->map(function ($field) use ($node) { - return new FieldValue($node, $field); - })->first(); + return (new SchemaBuilder())->build(ASTBuilder::generate($schema)); } /** - * Get field argument value. + * Convenience method to add a default Query, sometimes needed + * because the Schema is invalid without it. * - * @param string $name - * @param FieldValue $field + * @param string $schema * - * @return ArgumentValue + * @return \GraphQL\Type\Schema */ - protected function getFieldArg($name, FieldValue $field) + protected function buildSchemaWithDefaultQuery($schema) { - return collect(data_get($field->getField(), 'arguments', [])) - ->filter(function ($arg) use ($name) { - return $arg->name->value === $name; - })->map(function ($arg) use ($field) { - return new ArgumentValue($field, $arg); - })->first(); + return $this->buildSchemaFromString($schema.' + type Query { + dummy: String + } + '); } } diff --git a/tests/Unit/Schema/AST/ASTBuilderTest.php b/tests/Unit/Schema/AST/ASTBuilderTest.php new file mode 100644 index 0000000000..79ecff65bc --- /dev/null +++ b/tests/Unit/Schema/AST/ASTBuilderTest.php @@ -0,0 +1,28 @@ +assertCount(3, $ast->objectType('Query')->fields); + } +} diff --git a/tests/Unit/Schema/AST/PartialParserTest.php b/tests/Unit/Schema/AST/PartialParserTest.php new file mode 100644 index 0000000000..77bee563ce --- /dev/null +++ b/tests/Unit/Schema/AST/PartialParserTest.php @@ -0,0 +1,114 @@ +assertInstanceOf( + ObjectTypeDefinitionNode::class, + PartialParser::objectTypeDefinition(' + type Foo { + foo: String + } + ') + ); + } + + public function testThrowsForInvalidDefinition() + { + $this->expectException(SyntaxError::class); + PartialParser::objectTypeDefinition(' + INVALID + '); + } + + public function testThrowsIfMultipleDefinitionsAreGiven() + { + $this->expectException(ParseException::class); + PartialParser::objectTypeDefinition(' + type Foo { + foo: String + } + + type Bar { + bar: Int + } + '); + } + + public function testThrowsIfDefinitionIsUnexpectedType() + { + $this->expectException(ParseException::class); + PartialParser::objectTypeDefinition(' + interface Foo { + foo: String + } + '); + } + + public function testParsesObjectTypesArray() + { + $objectTypes = PartialParser::objectTypeDefinitions([' + type Foo { + foo: String + } + ', ' + type Bar { + bar: Int + } + ']); + + $this->assertCount(2, $objectTypes); + $this->assertInstanceOf(ObjectTypeDefinitionNode::class, $objectTypes[0]); + $this->assertInstanceOf(ObjectTypeDefinitionNode::class, $objectTypes[1]); + } + + public function testThrowsOnInvalidTypeInObjectTypesArray() + { + $this->expectException(ParseException::class); + PartialParser::objectTypeDefinitions([' + type Foo { + foo: String + } + ', ' + interface Bar { + bar: Int + } + ']); + } + + public function testThrowsOnMultipleDefinitionsInArrayItem() + { + $this->expectException(ParseException::class); + PartialParser::objectTypeDefinitions([' + type Foo { + foo: String + } + + type Bar { + bar: Int + } + ']); + } + + public function testParseOperationDefinition() + { + $this->assertInstanceOf( + OperationDefinitionNode::class, + PartialParser::operationDefinition(' + { + foo: Foo + } + ') + ); + } +} diff --git a/tests/Unit/Schema/AST/SchemaStitcherTest.php b/tests/Unit/Schema/AST/SchemaStitcherTest.php new file mode 100644 index 0000000000..a3906086bb --- /dev/null +++ b/tests/Unit/Schema/AST/SchemaStitcherTest.php @@ -0,0 +1,73 @@ +assertContains('type Baz', $schema); + } + + /** + * @test + */ + public function itCanImportSchemas() + { + $schema = SchemaStitcher::stitch(__DIR__.'/foo.graphql'); + $this->assertContains('type Foo', $schema); + $this->assertContains('type Bar', $schema); + $this->assertContains('type Baz', $schema); + } +} diff --git a/tests/Unit/Schema/DirectiveFactoryTest.php b/tests/Unit/Schema/DirectiveFactoryTest.php index 6fcd293157..ae56cb5e3b 100644 --- a/tests/Unit/Schema/DirectiveFactoryTest.php +++ b/tests/Unit/Schema/DirectiveFactoryTest.php @@ -4,6 +4,7 @@ use GraphQL\Language\Parser; use GraphQL\Type\Definition\ScalarType; +use Nuwave\Lighthouse\Schema\AST\PartialParser; use Nuwave\Lighthouse\Schema\Directives\Nodes\ScalarDirective; use Nuwave\Lighthouse\Schema\Values\NodeValue; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; @@ -51,14 +52,12 @@ public function itThrowsErrorIfMultipleDirectivesAssignedToNode() */ public function itCanCheckIfFieldHasAResolverDirective() { - $schema = ' + $type = PartialParser::objectTypeDefinition(' type Foo { bar: [Bar!]! @hasMany - } - '; + }'); - $document = Parser::parse($schema); - $hasResolver = directives()->hasResolver($document->definitions[0]->fields[0]); + $hasResolver = directives()->hasResolver($type->fields[0]); $this->assertTrue($hasResolver); } diff --git a/tests/Unit/Schema/Directives/Client/ClientDirectiveTest.php b/tests/Unit/Schema/Directives/Client/ClientDirectiveTest.php index 9521ff53bc..dcdf6101ad 100644 --- a/tests/Unit/Schema/Directives/Client/ClientDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Client/ClientDirectiveTest.php @@ -3,8 +3,6 @@ namespace Tests\Unit\Schema\Directives\Client; use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Utils\BuildSchema; -use Nuwave\Lighthouse\Schema\Resolvers\DirectiveResolver; use Tests\TestCase; class ClientDirectiveTest extends TestCase diff --git a/tests/Unit/Schema/Directives/Fields/AuthDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/AuthDirectiveTest.php index cdadaf62bf..3e0d99af83 100644 --- a/tests/Unit/Schema/Directives/Fields/AuthDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/AuthDirectiveTest.php @@ -17,16 +17,16 @@ public function itCanResolveAuthenticatedUser() }; $this->be($user); - $schema = ' + + $schema = $this->buildSchemaFromString(' type User { foo: String! } type Query { user: User! @auth - } - '; + }'); - $query = schema()->register($schema)->firstWhere('name', 'Query'); + $query = $schema->getType('Query'); $resolver = array_get($query->config['fields'](), 'user.resolve'); $this->assertEquals('bar', data_get($resolver(), 'foo')); } diff --git a/tests/Unit/Schema/Directives/Fields/CanDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/CanDirectiveTest.php index 3926858b43..265c09b05e 100644 --- a/tests/Unit/Schema/Directives/Fields/CanDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/CanDirectiveTest.php @@ -11,13 +11,11 @@ class CanDirectiveTest extends TestCase */ public function itCanAttachPoliciesToField() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @can(if: ["viewBar"]) - } - '; - - $type = schema()->register($schema)->first(); + }'); + $type = $schema->getType('Foo'); $fields = $type->config['fields']; $resolver = array_get($fields, 'bar.resolve'); // TODO: Use prophecy to ensure middleware is called diff --git a/tests/Unit/Schema/Directives/Fields/ComplexityDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/ComplexityDirectiveTest.php index 5a234a338a..fd6a51f74f 100644 --- a/tests/Unit/Schema/Directives/Fields/ComplexityDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/ComplexityDirectiveTest.php @@ -11,16 +11,15 @@ class ComplexityDirectiveTest extends TestCase */ public function itCanSetDefaultComplexityOnField() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type User { posts: [Post!]! @complexity @hasMany } type Post { title: String - } - '; + }'); - $type = schema()->register($schema)->first(); + $type = $schema->getType('User'); $fields = $type->config['fields'](); $complexity = $fields['posts']['complexity']; @@ -34,7 +33,8 @@ public function itCanSetDefaultComplexityOnField() public function itCanSetCustomComplexityResolver() { $resolver = addslashes(self::class); - $schema = ' + + $schema = $this->buildSchemaWithDefaultQuery(' type User { posts: [Post!]! @complexity(resolver: "'.$resolver.'@complexity") @@ -42,10 +42,8 @@ public function itCanSetCustomComplexityResolver() } type Post { title: String - } - '; - - $type = schema()->register($schema)->first(); + }'); + $type = $schema->getType('User'); $fields = $type->config['fields'](); $complexity = $fields['posts']['complexity']; $this->assertEquals(100, $complexity(10, ['foo' => 10])); diff --git a/tests/Unit/Schema/Directives/Fields/FieldDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/FieldDirectiveTest.php index 9b1bfb5507..52db40a16e 100644 --- a/tests/Unit/Schema/Directives/Fields/FieldDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/FieldDirectiveTest.php @@ -12,13 +12,12 @@ class FieldDirectiveTest extends TestCase */ public function itCanResolveFieldWithAssignedClass() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @field(class:"Tests\\\Utils\\\Resolvers\\\Foo" method: "bar") - } - '; + }'); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $resolve = array_get($fields, 'bar.resolve'); @@ -30,13 +29,13 @@ public function itCanResolveFieldWithAssignedClass() */ public function itCanResolveFieldWithMergedArgs() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @field(class:"Tests\\\Utils\\\Resolvers\\\Foo" method: "baz" args:["foo.baz"]) } - '; + '); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $resolve = array_get($fields, 'bar.resolve'); @@ -48,14 +47,14 @@ public function itCanResolveFieldWithMergedArgs() */ public function itThrowsAnErrorIfNoClassIsDefined() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @field(method: "bar") } - '; + '); $this->expectException(DirectiveException::class); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $type->config['fields'](); } @@ -64,14 +63,14 @@ public function itThrowsAnErrorIfNoClassIsDefined() */ public function itThrowsAnErrorIfNoMethodIsDefined() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @field(class: "Foo\\\Bar") } - '; + '); $this->expectException(DirectiveException::class); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $type->config['fields'](); } } diff --git a/tests/Unit/Schema/Directives/Fields/MethodDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/MethodDirectiveTest.php index 0d4d1dc04e..52f79d2ec2 100644 --- a/tests/Unit/Schema/Directives/Fields/MethodDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/MethodDirectiveTest.php @@ -18,13 +18,13 @@ public function foobar() } }; - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! @method(name: "foobar") } - '; + '); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $resolver = array_get($fields, 'bar.resolve'); $this->assertEquals('baz', $resolver($root, [])); @@ -42,13 +42,13 @@ public function bar(array $args) } }; - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar(baz: String!): String! @method(name: "bar") } - '; + '); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $resolver = array_get($fields, 'bar.resolve'); $this->assertEquals('foo', $resolver($root, ['baz' => 'foo'])); diff --git a/tests/Unit/Schema/Directives/Fields/MiddlewareDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/MiddlewareDirectiveTest.php index 7cb00ffbbd..2f3b69c1a0 100644 --- a/tests/Unit/Schema/Directives/Fields/MiddlewareDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/MiddlewareDirectiveTest.php @@ -11,7 +11,7 @@ class MiddlewareDirectiveTest extends TestCase */ public function itCanRegisterMiddleware() { - schema()->register(' + $schema = $this->buildSchemaFromString(' type Query { foo: String! @middleware(checks: ["auth:web", "auth:admin"]) bar: String! @@ -22,10 +22,6 @@ public function itCanRegisterMiddleware() } '); - collect(schema()->types())->each(function ($type) { - $type->config['fields'](); - }); - $query = 'query FooQuery { foo }'; $middleware = graphql()->middleware()->forRequest($query); $this->assertCount(2, $middleware); @@ -43,7 +39,7 @@ public function itCanRegisterMiddleware() */ public function itCanRegisterMiddlewareWithFragments() { - schema()->register(' + $schema = $this->buildSchemaFromString(' type Query { foo: String! @middleware(checks: ["auth:web", "auth:admin"]) bar: String! @@ -54,10 +50,6 @@ public function itCanRegisterMiddlewareWithFragments() } '); - collect(schema()->types())->each(function ($type) { - $type->config['fields'](); - }); - $query = 'query FooQuery { ...Foo_Fragment } fragment Foo_Fragment on Query { foo }'; $middleware = graphql()->middleware()->forRequest($query); $this->assertCount(2, $middleware); diff --git a/tests/Unit/Schema/Directives/Fields/PaginateDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/PaginateDirectiveTest.php new file mode 100644 index 0000000000..af1ea8537f --- /dev/null +++ b/tests/Unit/Schema/Directives/Fields/PaginateDirectiveTest.php @@ -0,0 +1,32 @@ +getConnectionQueryField('connection'); + $relay = $this->getConnectionQueryField('relay'); + + $this->assertEquals($connection, $relay); + } + + protected function getConnectionQueryField($type) + { + return $this->buildSchemaFromString(" + type Users { + name: String + } + + type Query { + users: [User!]! @paginate(type: \"$type\" model: \"User\") + } + ")->getQueryType()->getField('users'); + } +} diff --git a/tests/Unit/Schema/Directives/Fields/RenameDirectiveTest.php b/tests/Unit/Schema/Directives/Fields/RenameDirectiveTest.php index 0a67b9bf30..42d2577102 100644 --- a/tests/Unit/Schema/Directives/Fields/RenameDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Fields/RenameDirectiveTest.php @@ -12,13 +12,11 @@ class RenameDirectiveTest extends TestCase */ public function itCanRenameAField() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { fooBar: String! @rename(attribute: "foo_bar") - } - '; - - $type = schema()->register($schema)->first(); + }'); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $resolver = array_get($fields, 'fooBar.resolve'); $this->assertEquals('bar', $resolver(['foo_bar' => 'bar', 'fooBar' => 'baz'], [])); @@ -29,14 +27,13 @@ public function itCanRenameAField() */ public function itThrowsAnExceptionIfNoAttributeDefined() { - $schema = ' + $this->expectException(DirectiveException::class); + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { fooBar: String! @rename - } - '; + }'); - $this->expectException(DirectiveException::class); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $type->config['fields'](); } } diff --git a/tests/Unit/Schema/Directives/Nodes/GroupDirectiveTest.php b/tests/Unit/Schema/Directives/Nodes/GroupDirectiveTest.php index 83f7b695c3..324a63170e 100644 --- a/tests/Unit/Schema/Directives/Nodes/GroupDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Nodes/GroupDirectiveTest.php @@ -8,6 +8,7 @@ class GroupDirectiveTest extends TestCase { /** * @test + * @group fixing */ public function itCanSetNamespaces() { @@ -15,8 +16,7 @@ public function itCanSetNamespaces() type Query {} extend type Query @group(namespace: "Tests\\\Utils\\\Resolvers") { me: String @field(resolver: "Foo@bar") - } - '; + }'; $result = $this->execute($schema, '{ me }'); $this->assertEquals('foo.bar', $result->data['me']); diff --git a/tests/Unit/Schema/Directives/Nodes/SecurityDirectiveTest.php b/tests/Unit/Schema/Directives/Nodes/SecurityDirectiveTest.php index 9634f1cace..41da928f96 100644 --- a/tests/Unit/Schema/Directives/Nodes/SecurityDirectiveTest.php +++ b/tests/Unit/Schema/Directives/Nodes/SecurityDirectiveTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Schema\Directives\Nodes; use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; use Tests\TestCase; @@ -15,14 +14,13 @@ class SecurityDirectiveTest extends TestCase */ public function itCanSetMaxDepth() { - $schema = ' - type Query @security(depth: 3) { + $schema = $this->buildSchemaFromString(' + type Query @security(depth: 20) { me: String - }'; + }'); - schema()->register($schema); $rule = DocumentValidator::getRule(QueryDepth::class); - $this->assertEquals(3, $rule->getMaxQueryDepth()); + $this->assertEquals(20, $rule->getMaxQueryDepth()); } /** @@ -30,13 +28,12 @@ public function itCanSetMaxDepth() */ public function itCanSetMaxComplexity() { - $schema = ' - type Query @security(complexity: 3) { + $schema = $this->buildSchemaFromString(' + type Query @security(complexity: 20) { me: String - }'; + }'); - schema()->register($schema); $rule = DocumentValidator::getRule(QueryComplexity::class); - $this->assertEquals(3, $rule->getMaxQueryComplexity()); + $this->assertEquals(20, $rule->getMaxQueryComplexity()); } } diff --git a/tests/Unit/Schema/SchemaBuilderTest.php b/tests/Unit/Schema/SchemaBuilderTest.php index 502e3e03ef..f0deda65f1 100644 --- a/tests/Unit/Schema/SchemaBuilderTest.php +++ b/tests/Unit/Schema/SchemaBuilderTest.php @@ -37,7 +37,7 @@ public function getEnvironmentSetUp($app) */ public function itCanResolveEnumTypes() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' enum Role { # Company administrator. admin @enum(value:"admin") @@ -45,11 +45,9 @@ enum Role { # Company employee. employee @enum(value:"employee") } - '; + '); - $types = schema()->register($schema); - - $this->assertInstanceOf(EnumType::class, $types->firstWhere('name', 'Role')); + $this->assertInstanceOf(EnumType::class, $schema->getType('Role')); } /** @@ -57,15 +55,14 @@ enum Role { */ public function itCanResolveInterfaceTypes() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' interface Foo { # bar is baz bar: String! } - '; + '); - $types = schema()->register($schema); - $this->assertInstanceOf(InterfaceType::class, $types->firstWhere('name', 'Foo')); + $this->assertInstanceOf(InterfaceType::class, $schema->getType('Foo')); } /** @@ -73,13 +70,16 @@ interface Foo { */ public function itCanResolveScalarTypes() { - $schema = ' + $this->app['config']->set( + 'lighthouse.namespaces.scalars', + 'Nuwave\Lighthouse\Schema\Types\Scalars' + ); + + $schema = $this->buildSchemaWithDefaultQuery(' scalar DateTime @scalar(class:"DateTime") - '; + '); - $this->app['config']->set('lighthouse.namespaces.scalars', 'Nuwave\Lighthouse\Schema\Types\Scalars'); - $types = schema()->register($schema); - $this->assertInstanceOf(ScalarType::class, $types->firstWhere('name', 'DateTime')); + $this->assertInstanceOf(ScalarType::class, $schema->getType('DateTime')); } /** @@ -87,17 +87,17 @@ public function itCanResolveScalarTypes() */ public function itCanResolveObjectTypes() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { # bar attribute of Foo bar: String! } - '; + '); - $types = schema()->register($schema); - $this->assertInstanceOf(ObjectType::class, $types->firstWhere('name', 'Foo')); + $foo = $schema->getType('Foo'); + $this->assertInstanceOf(ObjectType::class, $foo); - $config = $types->firstWhere('name', 'Foo')->config; + $config = $foo->config; $this->assertEquals('Foo', data_get($config, 'name')); $this->assertEquals('bar attribute of Foo', array_get($config['fields'](), 'bar.description')); } @@ -107,17 +107,17 @@ public function itCanResolveObjectTypes() */ public function itCanResolveInputObjectTypes() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' input CreateFoo { foo: String! bar: Int } - '; + '); - $types = schema()->register($schema); - $this->assertInstanceOf(InputType::class, $types->firstWhere('name', 'CreateFoo')); + $createFoo = $schema->getType('CreateFoo'); + $this->assertInstanceOf(InputType::class, $createFoo); - $config = $types->firstWhere('name', 'CreateFoo')->config; + $config = $createFoo->config; $fields = $config['fields'](); $this->assertEquals('CreateFoo', data_get($config, 'name')); $this->assertArrayHasKey('foo', $fields); @@ -129,13 +129,13 @@ public function itCanResolveInputObjectTypes() */ public function itCanResolveMutations() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Mutation { foo(bar: String! baz: String): String } - '; + '); - $type = schema()->register($schema)->firstWhere('name', 'Mutation'); + $type = $schema->getType('Mutation'); $mutation = $type->config['fields']()['foo']; $this->assertArrayHasKey('args', $mutation); @@ -150,13 +150,13 @@ public function itCanResolveMutations() */ public function itCanResolveQueries() { - $schema = ' + $schema = $this->buildSchemaFromString(' type Query { foo(bar: String! baz: String): String } - '; + '); - $type = schema()->register($schema)->firstWhere('name', 'Query'); + $type = $schema->getType('Query'); $query = $type->config['fields']()['foo']; $this->assertArrayHasKey('args', $query); @@ -171,16 +171,16 @@ public function itCanResolveQueries() */ public function itCanExtendObjectTypes() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Foo { bar: String! } extend type Foo { baz: String! } - '; + '); - $type = schema()->register($schema)->first(); + $type = $schema->getType('Foo'); $fields = $type->config['fields'](); $this->assertArrayHasKey('baz', $fields); } @@ -190,16 +190,16 @@ public function itCanExtendObjectTypes() */ public function itCanExtendQuery() { - $schema = ' + $schema = $this->buildSchemaFromString(' type Query { foo: String! } extend type Query { bar: String! } - '; + '); - $type = schema()->register($schema)->firstWhere('name', 'Query'); + $type = $schema->getType('Query'); $fields = $type->config['fields'](); $this->assertArrayHasKey('bar', $fields); } @@ -209,16 +209,16 @@ public function itCanExtendQuery() */ public function itCanExtendMutation() { - $schema = ' + $schema = $this->buildSchemaWithDefaultQuery(' type Mutation { foo: String! } extend type Mutation { bar: String! } - '; + '); - $type = schema()->register($schema)->firstWhere('name', 'Mutation'); + $type = $schema->getType('Mutation'); $fields = $type->config['fields'](); $this->assertArrayHasKey('bar', $fields); } @@ -228,15 +228,18 @@ public function itCanExtendMutation() */ public function itCanGenerateGraphQLSchema() { - $schema = ' - type Query { - foo: String! - } - type Mutation { - foo: String! - } - '; - - $this->assertInstanceOf(Schema::class, schema()->build($schema)); + $schema = $this->buildSchemaFromString(' + type Query { + foo: String! + } + + type Mutation { + foo: String! + } + '); + + $this->assertInstanceOf(Schema::class, $schema); + // This would throw if the schema were invalid + $schema->assertValid(); } } diff --git a/tests/Unit/Schema/Utils/SchemaStitcherTest.php b/tests/Unit/Schema/Utils/SchemaStitcherTest.php deleted file mode 100644 index 22a9cb580e..0000000000 --- a/tests/Unit/Schema/Utils/SchemaStitcherTest.php +++ /dev/null @@ -1,100 +0,0 @@ -stitcher = new SchemaStitcher(); - - if (! is_dir(__DIR__.'/schema')) { - mkdir(__DIR__.'/schema'); - } - - file_put_contents(__DIR__.'/foo.graphql', ' - #import ./schema/bar.graphql - type Foo { - foo: String! - }'); - - file_put_contents(__DIR__.'/schema/bar.graphql', ' - #import ./baz.graphql - type Bar { - bar: String! - }'); - - file_put_contents(__DIR__.'/schema/baz.graphql', ' - type Baz { - baz: String! - }'); - } - - /** - * Tear down test case. - */ - protected function tearDown() - { - unlink(__DIR__.'/foo.graphql'); - unlink(__DIR__.'/schema/bar.graphql'); - unlink(__DIR__.'/schema/baz.graphql'); - - if (is_dir(__DIR__.'/schema')) { - rmdir(__DIR__.'/schema'); - } - } - - /** - * @test - */ - public function itStitchesLighthouseSchema() - { - $schema = $this->stitcher->stitch('_id'); - $hasNode = false !== strpos($schema, 'interface Node'); - - $this->assertTrue($hasNode); - } - - /** - * @test - */ - public function itConcatsSchemas() - { - $schema = $this->stitcher->stitch('_id', __DIR__.'/schema/baz.graphql'); - $hasNode = false !== strpos($schema, 'interface Node'); - $hasBaz = false !== strpos($schema, 'type Baz'); - - $this->assertTrue($hasNode); - $this->assertTrue($hasBaz); - } - - /** - * @test - */ - public function itCanImportSchemas() - { - $schema = $this->stitcher->stitch('_id', __DIR__.'/foo.graphql'); - $hasNode = false !== strpos($schema, 'interface Node'); - $hasFoo = false !== strpos($schema, 'type Foo'); - $hasBar = false !== strpos($schema, 'type Bar'); - $hasBaz = false !== strpos($schema, 'type Baz'); - - $this->assertTrue($hasNode, 'Schema does not include type Node'); - $this->assertTrue($hasFoo, 'Schema does not include type Foo'); - $this->assertTrue($hasBar, 'Schema does not include type Bar'); - $this->assertTrue($hasBaz, 'Schema does not include type Baz'); - } -} diff --git a/tests/Unit/Support/Validator/ValidatorTest.php b/tests/Unit/Support/Validator/ValidatorTest.php index e18b24d231..8f8c517958 100644 --- a/tests/Unit/Support/Validator/ValidatorTest.php +++ b/tests/Unit/Support/Validator/ValidatorTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Support\Validator; +use Nuwave\Lighthouse\Schema\AST\PartialParser; use Nuwave\Lighthouse\Schema\Directives\Args\ValidateDirective; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Schema\Values\NodeValue; @@ -28,17 +29,23 @@ protected function rules() }; }); - $field = $this->getNodeField(' + $typeDefinition = PartialParser::objectTypeDefinition(' type Mutation { foo(bar: String baz: Int): String @validate(validator: "foo.validator") } - ')->setResolver(function () { + '); + + $fieldValue = new FieldValue(new NodeValue($typeDefinition), $typeDefinition->fields[0]); + $fieldValue->setResolver(function () { return 'foo'; }); - (new ValidateDirective())->handleField($field); + (new ValidateDirective())->handleField($fieldValue); $this->expectException(ValidationError::class); - $field->getResolver()(null, ['bar' => 'foo', 'baz' => 1]); + $fieldValue->getResolver()( + null, + ['bar' => 'foo', 'baz' => 1] + ); } }