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

Allow @show, @hide, and @feature directives to be used on types, arguments and input types #2638

Merged
merged 7 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

## v6.46.0

### Added

- Allow `@show`, `@hide`, and `@feature` directives to be used on types, arguments and input types https://github.com/nuwave/lighthouse/pull/2638

## v6.45.1

### Fixed
Expand Down
22 changes: 19 additions & 3 deletions docs/6/digging-deeper/feature-toggles.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Feature Toggles

Lighthouse allows you to conditionally show or hide elements of your schema.
Lighthouse allows you to conditionally show or hide elements (fields, types, arguments or input fields) of your schema.

## @show and @hide

Expand All @@ -27,8 +27,8 @@ type Query {

## @feature

The [@feature](../api-reference/directives.md#feature) directive allows to include fields in the schema depending
on a [Laravel Pennant](https://laravel.com/docs/pennant) feature.
The [@feature](../api-reference/directives.md#feature) directive allows to include fields, types, arguments, or input fields in the schema
depending on whether a [Laravel Pennant](https://laravel.com/docs/pennant) feature is active.

For example, you might want a new experimental field only to be available when the according feature is active:

Expand Down Expand Up @@ -57,6 +57,22 @@ type Query {
}
```

## Conditional Type Inclusion

When you conditionally include a type using [@show](../api-reference/directives.md#show), [@hide](../api-reference/directives.md#hide) or [@feature](../api-reference/directives.md#feature),
any fields using it must also be conditionally included.
If the type is omitted but still used somewhere, the schema will be invalid.

```graphql
type ExperimentalType @feature(name: "new-api") {
field: String!
}

type Query {
experimentalField: ExperimentalType @feature(name: "new-api")
}
```

## Interaction With Schema Cache

[@show](../api-reference/directives.md#show) and [@hide](../api-reference/directives.md#hide) work by manipulating the schema.
Expand Down
22 changes: 19 additions & 3 deletions docs/master/digging-deeper/feature-toggles.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Feature Toggles

Lighthouse allows you to conditionally show or hide elements of your schema.
Lighthouse allows you to conditionally show or hide elements (fields, types, arguments or input fields) of your schema.

## @show and @hide

Expand All @@ -27,8 +27,8 @@ type Query {

## @feature

The [@feature](../api-reference/directives.md#feature) directive allows to include fields in the schema depending
on a [Laravel Pennant](https://laravel.com/docs/pennant) feature.
The [@feature](../api-reference/directives.md#feature) directive allows to include fields, types, arguments, or input fields in the schema
depending on whether a [Laravel Pennant](https://laravel.com/docs/pennant) feature is active.

For example, you might want a new experimental field only to be available when the according feature is active:

Expand Down Expand Up @@ -57,6 +57,22 @@ type Query {
}
```

## Conditional Type Inclusion

When you conditionally include a type using [@show](../api-reference/directives.md#show), [@hide](../api-reference/directives.md#hide) or [@feature](../api-reference/directives.md#feature),
any fields using it must also be conditionally included.
If the type is omitted but still used somewhere, the schema will be invalid.

```graphql
type ExperimentalType @feature(name: "new-api") {
field: String!
}

type Query {
experimentalField: ExperimentalType @feature(name: "new-api")
}
```

## Interaction With Schema Cache

[@show](../api-reference/directives.md#show) and [@hide](../api-reference/directives.md#hide) work by manipulating the schema.
Expand Down
4 changes: 2 additions & 2 deletions src/Pennant/FeatureDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Directives\HideDirective;

final class FeatureDirective extends HideDirective
class FeatureDirective extends HideDirective
{
public function __construct(
private FeatureManager $features,
Expand All @@ -30,7 +30,7 @@ public static function definition(): string
Specify what the state of the feature should be for the field to be included.
"""
when: FeatureState! = ACTIVE
) on FIELD_DEFINITION | OBJECT
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION | OBJECT
"""
Options for the `when` argument of `@feature`.
Expand Down
67 changes: 57 additions & 10 deletions src/Schema/Directives/HideDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
namespace Nuwave\Lighthouse\Schema\Directives;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use Illuminate\Container\Container;
use Illuminate\Contracts\Foundation\Application;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\InputFieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;

class HideDirective extends BaseDirective implements FieldManipulator
class HideDirective extends BaseDirective implements ArgManipulator, FieldManipulator, InputFieldManipulator, TypeManipulator
{
protected string $env;

Expand All @@ -36,7 +42,7 @@
Specify which environments must not use this field, e.g. ["production"].
"""
env: [String!]!
) repeatable on FIELD_DEFINITION
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION | OBJECT
GRAPHQL;
}

Expand All @@ -48,16 +54,57 @@

public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode &$parentType): void
{
if ($this->shouldHide()) {
$keyToRemove = null;
foreach ($parentType->fields as $key => $value) {
if ($value === $fieldDefinition) {
$keyToRemove = $key;
break;
}
if (! $this->shouldHide()) {
return;
}

foreach ($parentType->fields as $key => $value) {
if ($value === $fieldDefinition) {
unset($parentType->fields[$key]);
break;
}
}
}

public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
if (! $this->shouldHide()) {
return;

Check warning on line 72 in src/Schema/Directives/HideDirective.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/Directives/HideDirective.php#L72

Added line #L72 was not covered by tests
}

foreach ($documentAST->types as $key => $value) {
if ($value === $typeDefinition) {
unset($documentAST->types[$key]);
break;
}
}
}

public function manipulateInputFieldDefinition(DocumentAST &$documentAST, InputValueDefinitionNode &$inputField, InputObjectTypeDefinitionNode &$parentInput): void
{
if (! $this->shouldHide()) {
return;

Check warning on line 86 in src/Schema/Directives/HideDirective.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/Directives/HideDirective.php#L86

Added line #L86 was not covered by tests
}

foreach ($parentInput->fields as $key => $value) {
if ($value === $inputField) {
unset($parentInput->fields[$key]);
break;
}
}
}

unset($parentType->fields[$keyToRemove]);
public function manipulateArgDefinition(DocumentAST &$documentAST, InputValueDefinitionNode &$argDefinition, FieldDefinitionNode &$parentField, ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode &$parentType): void
{
if (! $this->shouldHide()) {
return;

Check warning on line 100 in src/Schema/Directives/HideDirective.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/Directives/HideDirective.php#L100

Added line #L100 was not covered by tests
}

foreach ($parentField->arguments as $key => $value) {
if ($value === $argDefinition) {
unset($parentField->arguments[$key]);
break;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Schema/Directives/ShowDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static function definition(): string
Specify which environments may use this field, e.g. ["testing"].
"""
env: [String!]!
) repeatable on FIELD_DEFINITION
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION | OBJECT
GRAPHQL;
}

Expand Down
87 changes: 87 additions & 0 deletions tests/Unit/Schema/Directives/HideDirectiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,93 @@ public function testHiddenOnTestingEnv(): void
$this->graphQL($query)->assertGraphQLErrorMessage('Cannot query field "hiddenField" on type "Query". Did you mean "shownField"?');
}

public function testHiddenArgs(): void
{
$this->schema = /** @lang GraphQL */ '
type Query {
field(hiddenArg: String @hide(env: ["testing"])): String! @mock
}
';

$introspectionQuery = /** @lang GraphQL */ '
{
__schema {
queryType {
fields {
args {
name
}
}
}
}
}
';

$this->graphQL($introspectionQuery)
->assertJsonCount(0, 'data.__schema.queryType.fields.0.args');
}

public function testHiddenType(): void
{
$this->schema = /** @lang GraphQL */ '
type Query {
field: String! @mock
}
type HiddenType @hide(env: ["testing"]) {
field: String!
}
';

$introspectionQuery = /** @lang GraphQL */ '
{
__schema {
types {
name
}
}
}
';

$types = $this->graphQL($introspectionQuery)
->json('data.__schema.types.*.name');

$this->assertNotContains('HiddenType', $types);
}

public function testHiddenInputField(): void
{
$this->schema = /** @lang GraphQL */ '
type Query {
field: String! @mock
}

input Input {
hiddenInputField: String! @hide(env: ["testing"])
}
';

$introspectionQuery = /** @lang GraphQL */ '
{
__schema {
types {
name
inputFields {
name
}
}
}
}
';

$types = $this->graphQL($introspectionQuery)
->json('data.__schema.types');

$input = array_filter($types, fn (array $type): bool => $type['name'] === 'Input');

$this->assertCount(1, $input);
$this->assertEmpty(current($input)['inputFields']);
}

public function testHiddenWhenManuallySettingEnv(): void
{
$this->schema = /** @lang GraphQL */ '
Expand Down
Loading