diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cd3bbd7..3d161d42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Fixed + +- Keep lazy type loading lazy on `webonyx/graphql-php >= 15.31.0` by passing scalar overrides explicitly when `SchemaConfig::setScalarOverrides` is available https://github.com/nuwave/lighthouse/pull/2772 + ## v6.69.0 ### Added diff --git a/src/Schema/SchemaBuilder.php b/src/Schema/SchemaBuilder.php index b6df5884f..5f6d6c702 100644 --- a/src/Schema/SchemaBuilder.php +++ b/src/Schema/SchemaBuilder.php @@ -70,6 +70,14 @@ protected function build(DocumentAST $documentAST): Schema fn (): array => $this->typeRegistry->possibleTypes(), ); + // Passing scalar overrides explicitly prevents the first lookup of a built-in scalar + // from discovering them by resolving the lazy types callable, which would eagerly + // build every type in the schema, see https://github.com/nuwave/lighthouse/issues/2771. + // TODO remove this check when the minimum version of webonyx/graphql-php includes the method + if (method_exists($config, 'setScalarOverrides')) { + $config->setScalarOverrides($this->typeRegistry->scalarOverrides()); + } + // There is no way to resolve directives lazily, so we convert them eagerly $directiveFactory = new DirectiveFactory( new ExecutableTypeNodeConverter($this->typeRegistry), diff --git a/src/Schema/TypeRegistry.php b/src/Schema/TypeRegistry.php index 9d23e6a55..676cedcb9 100644 --- a/src/Schema/TypeRegistry.php +++ b/src/Schema/TypeRegistry.php @@ -239,6 +239,32 @@ public function possibleTypes(): array return array_filter($this->types); } + /** + * Built-in scalar types that are overridden in this schema. + * + * A scalar override is a type named after a built-in scalar such as `String`, + * defined in the schema or registered programmatically. + * + * @return array + */ + public function scalarOverrides(): array + { + $overrides = []; + foreach (Type::BUILT_IN_SCALAR_NAMES as $name) { + if ( + isset($this->types[$name]) + || isset($this->documentAST->types[$name]) + || isset($this->lazyTypes[$name]) + ) { + $override = $this->get($name); + assert($override instanceof ScalarType); + $overrides[] = $override; + } + } + + return $overrides; + } + /** * Get the types that are currently resolved. * diff --git a/tests/Unit/Schema/SchemaBuilderTest.php b/tests/Unit/Schema/SchemaBuilderTest.php index a9962c8a5..6d341e0f0 100644 --- a/tests/Unit/Schema/SchemaBuilderTest.php +++ b/tests/Unit/Schema/SchemaBuilderTest.php @@ -8,9 +8,13 @@ use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; +use GraphQL\Type\Definition\Type; use Nuwave\Lighthouse\Schema\RootType; use Nuwave\Lighthouse\Schema\SchemaBuilder; +use Nuwave\Lighthouse\Schema\TypeRegistry; use Tests\TestCase; +use Tests\Utils\Scalars\Email; final class SchemaBuilderTest extends TestCase { @@ -209,6 +213,79 @@ public function testExtendTypes(): void $this->assertSame('yo?', $type->getField('bar')->description); } + public function testRegistersExplicitlyEmptyScalarOverrides(): void + { + $schema = $this->buildSchemaWithPlaceholderQuery(''); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $this->assertSame([], $config->getScalarOverrides()); + } + + public function testRegistersRedefinedBuiltInScalarsAsScalarOverrides(): void + { + $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' + scalar String @scalar(class: "Email") + '); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $scalarOverrides = $config->getScalarOverrides(); + $this->assertNotNull($scalarOverrides); + $this->assertCount(1, $scalarOverrides); + + $stringOverride = $scalarOverrides[Type::STRING]; + $this->assertInstanceOf(Email::class, $stringOverride); + $this->assertSame(Type::STRING, $stringOverride->name); + } + + public function testRegistersProgrammaticallyOverwrittenBuiltInScalarsAsScalarOverrides(): void + { + $typeRegistry = $this->app->make(TypeRegistry::class); + $typeRegistry->overwrite(new Email(['name' => Type::STRING])); + + $schema = $this->buildSchemaWithPlaceholderQuery(''); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $scalarOverrides = $config->getScalarOverrides(); + $this->assertNotNull($scalarOverrides); + $this->assertCount(1, $scalarOverrides); + + $stringOverride = $scalarOverrides[Type::STRING]; + $this->assertInstanceOf(Email::class, $stringOverride); + $this->assertSame(Type::STRING, $stringOverride->name); + } + + public function testBuiltInScalarLookupDoesNotResolveAllTypes(): void + { + $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' + type Foo { + bar: Int + } + '); + + $config = $schema->getConfig(); + if (! method_exists($config, 'getScalarOverrides')) { + $this->markTestSkipped('Requires a version of webonyx/graphql-php that supports SchemaConfig::setScalarOverrides.'); + } + + $booleanType = $schema->getType(Type::BOOLEAN); + $this->assertInstanceOf(ScalarType::class, $booleanType); + + $typeRegistry = $this->app->make(TypeRegistry::class); + $this->assertArrayNotHasKey('Foo', $typeRegistry->resolvedTypes()); + } + public function testResolvesEnumDefaultValuesToInternalValues(): void { $schema = $this->buildSchema(/** @lang GraphQL */ <<<'GRAPHQL'