Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add directive @void to unify the definition of fields with no return value #1992

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

## v5.28.0

### Added

- Add directive `@void` to unify the definition of fields with no return value
- Mixin method `assertGraphQLErrorFree()` to `\Illuminate\Testing\TestResponse`

### Fixed

- Add missing types to `programmatic-types.graphql` in artisan command `lighthouse:ide-helper`

## v5.27.1

### Changed
Expand Down
10 changes: 10 additions & 0 deletions _ide_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ public function assertGraphQLErrorMessage(string $message): self
return $this;
}

/**
* Assert the response contains no errors.
*
* @return $this
*/
public function assertGraphQLErrorFree(): self
{
return $this;
}

/**
* Assert the response contains an error from the given category.
*
Expand Down
2 changes: 1 addition & 1 deletion docs/5/performance/schema-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using the [cache](../api-reference/commands.md#cache) artisan command:

The structure of the serialized schema can change between Lighthouse releases.
In order to prevent errors, use cache version 2 and a deployment method that
atomically updates both the cache file and the dependencies, e.g. K8s.
atomically updates both the cache file and the dependencies, e.g. K8s.

## Development

Expand Down
41 changes: 41 additions & 0 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -3142,6 +3142,47 @@ directive @validator(

Read more in the [validation docs](../security/validation.md#validator-classes).

## @void

```graphql
"""
Mark a field that returns no value.

The return type of the field will be changed to `Unit!`, defined as `enum Unit { UNIT }`.
Whatever result is returned from the resolver will be replaced with `UNIT`.
"""
directive @void on FIELD_DEFINITION
```

To enable this directive, add the service provider to your `config/app.php`:

```php
'providers' => [
\Nuwave\Lighthouse\Void\VoidServiceProvider::class,
],
```

Lighthouse will register the following type in your schema:

```graphql
"Allows only one value and thus can hold no information."
enum Unit {
"The only possible value."
UNIT
}
```

Use this directive on mutations that return no value, see [motivation](https://github.com/graphql/graphql-spec/issues/906).

```graphql
type Mutation {
fireAndForget: _ @void
}
```

Lighthouse will modify the definition of your field and have it return `Unit!`.
No matter what your resolver returns, the resulting value will always be `UNIT`.

## @where

```graphql
Expand Down
2 changes: 1 addition & 1 deletion docs/master/performance/schema-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using the [cache](../api-reference/commands.md#cache) artisan command:

The structure of the serialized schema can change between Lighthouse releases.
In order to prevent errors, use cache version 2 and a deployment method that
atomically updates both the cache file and the dependencies, e.g. K8s.
atomically updates both the cache file and the dependencies, e.g. K8s.

## Development

Expand Down
61 changes: 41 additions & 20 deletions src/Console/IdeHelperCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace Nuwave\Lighthouse\Console;

use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\SchemaPrinter;
use HaydenPierce\ClassFinder\ClassFinder;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Schema\AST\ASTCache;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\Contracts\Directive;

class IdeHelperCommand extends Command
Expand All @@ -28,11 +31,11 @@ class IdeHelperCommand extends Command

protected $description = 'Create IDE helper files to improve type checking and autocompletion.';

public function handle(DirectiveLocator $directiveLocator, TypeRegistry $typeRegistry): int
public function handle(): int
{
$this->schemaDirectiveDefinitions($directiveLocator);
$this->programmaticTypes($typeRegistry);
$this->phpIdeHelper();
$this->laravel->call([$this, 'schemaDirectiveDefinitions']);
$this->laravel->call([$this, 'programmaticTypes']);
$this->laravel->call([$this, 'phpIdeHelper']);

$this->info("\nIt is recommended to add them to your .gitignore file.");

Expand All @@ -42,7 +45,7 @@ public function handle(DirectiveLocator $directiveLocator, TypeRegistry $typeReg
/**
* Create and write schema directive definitions to a file.
*/
protected function schemaDirectiveDefinitions(DirectiveLocator $directiveLocator): void
public function schemaDirectiveDefinitions(DirectiveLocator $directiveLocator): void
{
$schema = /** @lang GraphQL */ <<<'GRAPHQL'
"""
Expand Down Expand Up @@ -131,28 +134,46 @@ public static function schemaDirectivesPath(): string
return base_path().'/schema-directives.graphql';
}

protected function programmaticTypes(TypeRegistry $typeRegistry): void
/**
* Users may register types programmatically, e.g. in service providers.
* In order to allow referencing those in the schema, it is useful to print
* those types to a helper schema, excluding types the user defined in the schema.
*/
public function programmaticTypes(SchemaSourceProvider $schemaSourceProvider, ASTCache $astCache, SchemaBuilder $schemaBuilder): void
{
// Users may register types programmatically, e.g. in service providers
// In order to allow referencing those in the schema, it is useful to print
// those types to a helper schema, excluding types the user defined in the schema
$types = new Collection($typeRegistry->resolvedTypes());
$sourceSchema = Parser::parse($schemaSourceProvider->getSchemaString());
$sourceTypes = [];
foreach ($sourceSchema->definitions as $definition) {
if ($definition instanceof TypeDefinitionNode) {
$sourceTypes[$definition->name->value] = true;
}
}

$astCache->clear();

$allTypes = $schemaBuilder->schema()->getTypeMap();

$programmaticTypes = array_diff_key($allTypes, $sourceTypes);

$filePath = static::programmaticTypesPath();

if ($types->isEmpty() && file_exists($filePath)) {
if (count($programmaticTypes) === 0 && file_exists($filePath)) {
\Safe\unlink($filePath);

return;
}

$schema = $types
->map(function (Type $type): string {
return SchemaPrinter::printType($type);
})
->implode("\n");
$schema = implode(
"\n\n",
array_map(
function (Type $type): string {
return SchemaPrinter::printType($type);
},
$programmaticTypes
)
);

\Safe\file_put_contents($filePath, self::GENERATED_NOTICE.$schema);
\Safe\file_put_contents($filePath, self::GENERATED_NOTICE.$schema."\n");

$this->info("Wrote definitions for programmatically registered types to $filePath.");
}
Expand All @@ -162,7 +183,7 @@ public static function programmaticTypesPath(): string
return base_path().'/programmatic-types.graphql';
}

protected function phpIdeHelper(): void
public function phpIdeHelper(): void
{
$filePath = static::phpIdeHelperPath();
$contents = \Safe\file_get_contents(__DIR__.'/../../_ide_helper.php');
Expand Down
3 changes: 1 addition & 2 deletions src/Schema/SchemaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ function (string $name): Type {
}
);

// This is just used for introspection, it is required
// to be able to retrieve all the types in the schema
// Enables introspection to list all types in the schema
$config->setTypes(
/**
* @return array<string, \GraphQL\Type\Definition\Type>
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Source/SchemaSourceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
interface SchemaSourceProvider
{
/**
* Provide the schema definition.
* Provide the string contents of the schema definition.
*/
public function getSchemaString(): string;
}
2 changes: 1 addition & 1 deletion src/Schema/TypeRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public function possibleTypes(): array
/**
* Get the types that are currently resolved.
*
* Note that this does not all possible types, only those that
* This does not return all possible types, only those that
* are programmatically registered or already resolved.
*
* @return array<string, \GraphQL\Type\Definition\Type>
Expand Down
13 changes: 13 additions & 0 deletions src/Testing/TestResponseMixin.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ public function assertGraphQLErrorMessage(): Closure
};
}

public function assertGraphQLErrorFree(): Closure
{
return function () {
$errors = $this->json('errors');
Assert::assertNull(
$errors,
'Expected the GraphQL response to contain no errors, got: '.\Safe\json_encode($errors)
);

return $this;
};
}

public function assertGraphQLErrorCategory(): Closure
{
return function (string $category) {
Expand Down
43 changes: 43 additions & 0 deletions src/Void/VoidDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Nuwave\Lighthouse\Void;

use Closure;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\Parser;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;

class VoidDirective extends BaseDirective implements FieldManipulator, FieldMiddleware
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Mark a field that returns no value.

The return type of the field will be changed to `Unit!`, defined as `enum Unit { UNIT }`.
Whatever result is returned from the resolver will be replaced with `UNIT`.
"""
directive @void on FIELD_DEFINITION
GRAPHQL;
}

public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType)
{
$fieldDefinition->type = Parser::typeReference(/** @lang GraphQL */ 'Unit!');
}

public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$fieldValue->resultHandler(static function (): string {
return VoidServiceProvider::UNIT;
});

return $fieldValue;
}
}
44 changes: 44 additions & 0 deletions src/Void/VoidServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Nuwave\Lighthouse\Void;

use GraphQL\Language\Parser;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;

/**
* TODO include by default in v6.
*/
class VoidServiceProvider extends ServiceProvider
{
public const UNIT = 'UNIT';

public function boot(Dispatcher $dispatcher): void
{
$dispatcher->listen(
ManipulateAST::class,
static function (ManipulateAST $manipulateAST): void {
$unit = self::UNIT;
$manipulateAST->documentAST->setTypeDefinition(
Parser::enumTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
"Allows only one value and thus can hold no information."
enum Unit {
"The only possible value."
{$unit}
}
GRAPHQL
)
);
}
);

$dispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return __NAMESPACE__;
}
);
}
}
33 changes: 18 additions & 15 deletions tests/Integration/GraphQLTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function testResolvesQueryViaPostRequest(): void
foo
}
')
->assertGraphQLErrorFree()
->assertExactJson([
'data' => [
'foo' => Foo::THE_ANSWER,
Expand Down Expand Up @@ -54,21 +55,23 @@ public function testResolvesQueryViaGetRequest(): void

public function testResolvesNamedOperation(): void
{
$this->postGraphQL([
'query' => /** @lang GraphQL */ '
query Foo {
foo
}
query Bar {
bar
}
',
'operationName' => 'Bar',
])->assertExactJson([
'data' => [
'bar' => Bar::RESULT,
],
]);
$this
->postGraphQL([
'query' => /** @lang GraphQL */ '
query Foo {
foo
}
query Bar {
bar
}
',
'operationName' => 'Bar',
])
->assertExactJson([
'data' => [
'bar' => Bar::RESULT,
],
]);
}

public function testResolveBatchedQueries(): void
Expand Down
Loading