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 empty root types automatically when extending them #1347

Merged
merged 4 commits into from May 3, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,11 @@ You can find and compare releases at the [GitHub release page](https://github.co
### Added

- Allow passing additional headers to `multipartGraphQL` test helper https://github.com/nuwave/lighthouse/pull/1342
- Add empty root types automatically when extending them https://github.com/nuwave/lighthouse/pull/1347

### Changed

- Improve validation error when extending a type that is not defined https://github.com/nuwave/lighthouse/pull/1347

## 4.12.4

Expand Down
26 changes: 1 addition & 25 deletions docs/master/digging-deeper/schema-organisation.md
Expand Up @@ -97,32 +97,8 @@ extend type Query {

The fields in the `extend type` definition are merged with those of the original type.

### Root Definitions

A valid `Query` type definition with at least one field must be present in the root schema.
This is because `extend type` needs the original type to get merged into.

You can provide an empty `Query` type (without curly braces) in the root schema:

```graphql
type Query

#import post.graphql
```

The same applies for mutations: if you want to use them, you can define
an empty `Mutation` type (without curly braces) within your root schema:

```graphql
type Query

type Mutation

#import post.graphql
```

### Extending other types

Apart from object types, you can also extend `input`, `interface` and `enum` types.
Apart from object types `type`, you can also extend `input`, `interface` and `enum` types.
Lighthouse will merge the fields (or values) with the original definition and always
produce a single type in the final schema.
3 changes: 2 additions & 1 deletion src/Defer/DeferrableDirective.php
Expand Up @@ -10,6 +10,7 @@
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\ClientDirectives\ClientDirective;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
Expand Down Expand Up @@ -81,7 +82,7 @@ protected function shouldDefer(TypeNode $fieldType, ResolveInfo $resolveInfo): b
$defers = (new ClientDirective(self::DEFER_DIRECTIVE_NAME))->forField($resolveInfo);

if ($this->anyFieldHasDefer($defers)) {
if ($resolveInfo->parentType->name === 'Mutation') {
if ($resolveInfo->parentType->name === RootType::MUTATION) {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD);
}
if ($fieldType instanceof NonNullTypeNode) {
Expand Down
53 changes: 37 additions & 16 deletions src/Schema/AST/ASTBuilder.php
Expand Up @@ -19,6 +19,7 @@
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
Expand Down Expand Up @@ -203,8 +204,17 @@ protected function applyTypeExtensionManipulators(): void
*/
protected function extendObjectLikeType(string $typeName, TypeExtensionNode $typeExtension): void
{
/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode $extendedObjectLikeType */
$extendedObjectLikeType = $this->documentAST->types[$typeName];
/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode|null $extendedObjectLikeType */
$extendedObjectLikeType = $this->documentAST->types[$typeName] ?? null;
if ($extendedObjectLikeType === null) {
if (RootType::isRootType($typeName)) {
$extendedObjectLikeType = PartialParser::objectTypeDefinition(/** @lang GraphQL */ "type {$typeName}");
$this->documentAST->setTypeDefinition($extendedObjectLikeType);
} else {
$this->throwDefinitionDoesNotExist($typeName, $typeExtension);
}
}

$this->assertExtensionMatchesDefinition($typeExtension, $extendedObjectLikeType);

$extendedObjectLikeType->fields = ASTHelper::mergeUniqueNodeList(
Expand All @@ -215,8 +225,12 @@ protected function extendObjectLikeType(string $typeName, TypeExtensionNode $typ

protected function extendEnumType(string $typeName, EnumTypeExtensionNode $typeExtension): void
{
/** @var \GraphQL\Language\AST\EnumTypeDefinitionNode $extendedEnum */
$extendedEnum = $this->documentAST->types[$typeName];
/** @var \GraphQL\Language\AST\EnumTypeDefinitionNode|null $extendedEnum */
$extendedEnum = $this->documentAST->types[$typeName] ?? null;
if ($extendedEnum === null) {
$this->throwDefinitionDoesNotExist($typeName, $typeExtension);
}

$this->assertExtensionMatchesDefinition($typeExtension, $extendedEnum);

$extendedEnum->values = ASTHelper::mergeUniqueNodeList(
Expand All @@ -225,6 +239,22 @@ protected function extendEnumType(string $typeName, EnumTypeExtensionNode $typeE
);
}

/**
* @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode|\GraphQL\Language\AST\EnumTypeExtensionNode $typeExtension
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
protected function throwDefinitionDoesNotExist(string $typeName, TypeExtensionNode $typeExtension): void
{
throw new DefinitionException(
"Could not find a base definition $typeName of kind {$typeExtension->kind} to extend."
);
}

/**
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/

/**
* @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode|\GraphQL\Language\AST\EnumTypeExtensionNode $extension
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode|\GraphQL\Language\AST\EnumTypeDefinitionNode $definition
Expand All @@ -235,20 +265,11 @@ protected function assertExtensionMatchesDefinition(TypeExtensionNode $extension
{
if (static::EXTENSION_TO_DEFINITION_CLASS[get_class($extension)] !== get_class($definition)) {
throw new DefinitionException(
static::extensionDoesNotMatchDefinition($extension, $definition)
"The type extension {$extension->name->value} of kind {$extension->kind} can not extend a definition of kind {$definition->kind}."
);
}
}

/**
* @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode|\GraphQL\Language\AST\EnumTypeExtensionNode $extension
* @param \GraphQL\Language\AST\ScalarTypeDefinitionNode|\GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode|\GraphQL\Language\AST\UnionTypeDefinitionNode|\GraphQL\Language\AST\EnumTypeDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode $definition
*/
public static function extensionDoesNotMatchDefinition(TypeExtensionNode $extension, TypeDefinitionNode $definition): string
{
return 'The type extension '.$extension->name->value.' of kind '.$extension->kind.' can not extend a definition of kind '.$definition->kind.'.';
}

/**
* Apply directives on fields that can manipulate the AST.
*/
Expand Down Expand Up @@ -404,8 +425,8 @@ interface Node @interface(resolveType: "Nuwave\\\Lighthouse\\\Schema\\\NodeRegis
)
);

/** @var ObjectTypeDefinitionNode $queryType */
$queryType = $this->documentAST->types['Query'];
/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode $queryType */
$queryType = $this->documentAST->types[RootType::QUERY];
$queryType->fields = ASTHelper::mergeNodeList(
$queryType->fields,
[
Expand Down
3 changes: 2 additions & 1 deletion src/Schema/Directives/CacheDirective.php
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Cache\CacheManager;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\Values\CacheValue;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Schema\Values\TypeValue;
Expand Down Expand Up @@ -133,7 +134,7 @@ protected function setCacheKeyOnParent(TypeValue $typeValue): void
// The cache key was already set, so we do not have to look again
$typeValue->getCacheKey()
// The Query type is exempt from requiring a cache key
|| $typeValue->getTypeDefinitionName() === 'Query'
|| $typeValue->getTypeDefinitionName() === RootType::QUERY
) {
return;
}
Expand Down
22 changes: 22 additions & 0 deletions src/Schema/RootType.php
@@ -0,0 +1,22 @@
<?php

namespace Nuwave\Lighthouse\Schema;

class RootType
{
public const QUERY = 'Query';
public const MUTATION = 'Mutation';
public const SUBSCRIPTION = 'Subscription';

public static function isRootType(string $typeName): bool
{
return in_array(
$typeName,
[
self::QUERY,
self::MUTATION,
self::SUBSCRIPTION,
]
);
}
}
10 changes: 5 additions & 5 deletions src/Schema/SchemaBuilder.php
Expand Up @@ -39,20 +39,20 @@ public function build(DocumentAST $documentAST): Schema

// Always set Query since it is required
/** @var \GraphQL\Type\Definition\ObjectType $query */
$query = $this->typeRegistry->get('Query');
$query = $this->typeRegistry->get(RootType::QUERY);
$config->setQuery($query);

// Mutation and Subscription are optional, so only add them
// if they are present in the schema
if (isset($documentAST->types['Mutation'])) {
if (isset($documentAST->types[RootType::MUTATION])) {
/** @var \GraphQL\Type\Definition\ObjectType $mutation */
$mutation = $this->typeRegistry->get('Mutation');
$mutation = $this->typeRegistry->get(RootType::MUTATION);
$config->setMutation($mutation);
}

if (isset($documentAST->types['Subscription'])) {
if (isset($documentAST->types[RootType::SUBSCRIPTION])) {
/** @var \GraphQL\Type\Definition\ObjectType $subscription */
$subscription = $this->typeRegistry->get('Subscription');
$subscription = $this->typeRegistry->get(RootType::SUBSCRIPTION);
// Eager-load the subscription fields to ensure they are registered
$subscription->getFields();

Expand Down
14 changes: 6 additions & 8 deletions src/Schema/Values/FieldValue.php
Expand Up @@ -7,6 +7,7 @@
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\Type;
use Nuwave\Lighthouse\Schema\ExecutableTypeNodeConverter;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver;
use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver;

Expand Down Expand Up @@ -79,7 +80,7 @@ public function setResolver(callable $resolver): self
*/
public function useDefaultResolver(): self
{
$this->resolver = $this->getParentName() === 'Subscription'
$this->resolver = $this->getParentName() === RootType::SUBSCRIPTION
? app(ProvidesSubscriptionResolver::class)->provideSubscriptionResolver($this)
: app(ProvidesResolver::class)->provideResolver($this);

Expand Down Expand Up @@ -161,11 +162,11 @@ public function getResolver(): ?callable
public function defaultNamespacesForParent(): array
{
switch ($this->getParentName()) {
case 'Query':
case RootType::QUERY:
return (array) config('lighthouse.namespaces.queries');
case 'Mutation':
case RootType::MUTATION:
return (array) config('lighthouse.namespaces.mutations');
case 'Subscription':
case RootType::SUBSCRIPTION:
return (array) config('lighthouse.namespaces.subscriptions');
default:
return [];
Expand Down Expand Up @@ -200,9 +201,6 @@ public function getDeprecationReason(): ?string
*/
public function parentIsRootType(): bool
{
return in_array(
$this->getParentName(),
['Query', 'Mutation', 'Subscription']
);
return RootType::isRootType($this->getParentName());
}
}
5 changes: 3 additions & 2 deletions tests/Integration/IntrospectionTest.php
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Integration;

use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Tests\TestCase;
use Tests\Utils\Scalars\Email;
Expand All @@ -27,7 +28,7 @@ protected function setUp(): void

public function testFindsTypesFromSchema(): void
{
$this->schema .= '
$this->schema .= /** @lang GraphQL */ '
type Foo {
bar: Int
}
Expand All @@ -37,7 +38,7 @@ public function testFindsTypesFromSchema(): void
$this->introspectType('Foo')
);
$this->assertNotNull(
$this->introspectType('Query')
$this->introspectType(RootType::QUERY)
);

$this->assertNull(
Expand Down
3 changes: 2 additions & 1 deletion tests/Unit/Execution/Arguments/ArgumentSetFactoryTest.php
Expand Up @@ -10,6 +10,7 @@
use Nuwave\Lighthouse\Execution\Arguments\NamedType;
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\RootType;
use Tests\TestCase;

class ArgumentSetFactoryTest extends TestCase
Expand Down Expand Up @@ -174,7 +175,7 @@ protected function rootQueryArgumentSet(array $args): ArgumentSet
$documentAST = $astBuilder->documentAST();

/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode $queryType */
$queryType = $documentAST->types['Query'];
$queryType = $documentAST->types[RootType::QUERY];

/** @var \GraphQL\Language\AST\FieldDefinitionNode $fooField */
$fooField = ASTHelper::firstByName($queryType->fields, 'foo');
Expand Down
51 changes: 50 additions & 1 deletion tests/Unit/Schema/AST/ASTBuilderTest.php
Expand Up @@ -6,6 +6,7 @@
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\RootType;
use Tests\TestCase;

class ASTBuilderTest extends TestCase
Expand Down Expand Up @@ -41,7 +42,38 @@ public function testCanMergeTypeExtensionFields(): void

$this->assertCount(
3,
$documentAST->types['Query']->fields
$documentAST->types[RootType::QUERY]->fields
);
}

public function testAllowsExtendingUndefinedRootTypes(): void
{
$this->schema = /** @lang GraphQL */ '
extend type Query {
foo: ID
}

extend type Mutation {
bar: ID
}

extend type Subscription {
baz: ID
}
';
$documentAST = $this->astBuilder->documentAST();

$this->assertCount(
1,
$documentAST->types[RootType::QUERY]->fields
);
$this->assertCount(
1,
$documentAST->types[RootType::MUTATION]->fields
);
$this->assertCount(
1,
$documentAST->types[RootType::SUBSCRIPTION]->fields
);
}

Expand Down Expand Up @@ -115,6 +147,23 @@ enum MyEnum {
);
}

public function testDoesNotAllowExtendingUndefinedTypes(): void
{
$this->schema = /** @lang GraphQL */ '
type Query {
foo: String
}

extend type Foo {
foo: Int
}
';

$this->expectException(DefinitionException::class);
$this->expectExceptionMessage('Could not find a base definition Foo of kind '.NodeKind::OBJECT_TYPE_EXTENSION.' to extend.');
$this->astBuilder->documentAST();
}

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