-
Notifications
You must be signed in to change notification settings - Fork 9
feat: entity type customization, type loader support #33
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,17 +4,17 @@ | |
|
||
namespace Apollo\Federation; | ||
|
||
use Apollo\Federation\Types\AnyType; | ||
use GraphQL\Type\Schema; | ||
use GraphQL\Type\Definition\CustomScalarType; | ||
use GraphQL\Type\Definition\Directive; | ||
use GraphQL\Type\Definition\ObjectType; | ||
use GraphQL\Type\Definition\UnionType; | ||
use GraphQL\Type\Definition\Type; | ||
use GraphQL\Utils\TypeInfo; | ||
use GraphQL\Utils\Utils; | ||
|
||
use Apollo\Federation\Types\EntityObjectType; | ||
use Apollo\Federation\Utils\FederatedSchemaPrinter; | ||
use Apollo\Federation\Types\EntityUnionType; | ||
use Apollo\Federation\Types\ServiceDefinitionType; | ||
|
||
/** | ||
* A federated GraphQL schema definition (see [related docs](https://www.apollographql.com/docs/apollo-server/federation/introduction)) | ||
|
@@ -55,18 +55,36 @@ | |
*/ | ||
class FederatedSchema extends Schema | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring for the class has not been updated to include this new functionality. That's not required, but would be useful |
||
{ | ||
/** @var EntityObjectType[] */ | ||
/** @var EntityObjectType[]|callable: EntityObjectType[] */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't seen the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually adapted it from here: https://github.com/webonyx/graphql-php/blob/master/src/Type/Definition/UnionType.php#L18 I'm not sure when it was introduced, but I think it's apt that we're mimic'ing the union type structure. |
||
protected $entityTypes; | ||
|
||
/** @var Directive[] */ | ||
protected $entityDirectives; | ||
|
||
protected ServiceDefinitionType $serviceDefinitionType; | ||
protected EntityUnionType $entityUnionType; | ||
protected AnyType $anyType; | ||
|
||
/** | ||
* | ||
* We will provide the parts that we need to operate against. | ||
* | ||
* @param array{?entityTypes: array<EntityObjectType>, ?typeLoader: callable, query: array} $config | ||
*/ | ||
public function __construct($config) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the signature of the config array? It didn't exist before but it certainly can now, especially as we introduce even more properties. |
||
{ | ||
$this->entityTypes = $this->extractEntityTypes($config); | ||
$this->entityTypes = $config['entityTypes'] ?? $this->lazyEntityTypeExtractor($config); | ||
$this->entityDirectives = array_merge(Directives::getDirectives(), Directive::getInternalDirectives()); | ||
|
||
$config = array_merge($config, $this->getEntityDirectivesConfig($config), $this->getQueryTypeConfig($config)); | ||
|
||
$this->serviceDefinitionType = new ServiceDefinitionType($this); | ||
$this->entityUnionType = new EntityUnionType($this->entityTypes); | ||
$this->anyType = new AnyType(); | ||
|
||
$config = array_merge($config, | ||
$this->getEntityDirectivesConfig($config), | ||
$this->getQueryTypeConfig($config), | ||
$this->supplementTypeLoader($config) | ||
); | ||
|
||
parent::__construct($config); | ||
} | ||
|
@@ -78,7 +96,9 @@ public function __construct($config) | |
*/ | ||
public function getEntityTypes(): array | ||
{ | ||
return $this->entityTypes; | ||
return is_callable($this->entityTypes) | ||
? ($this->entityTypes)() | ||
: $this->entityTypes; | ||
} | ||
|
||
/** | ||
|
@@ -121,24 +141,42 @@ private function getQueryTypeConfig(array $config): array | |
]; | ||
} | ||
|
||
/** | ||
* Add type loading functionality for the types required for the federated schema to function. | ||
*/ | ||
private function supplementTypeLoader(array $config): array | ||
{ | ||
if (!array_key_exists('typeLoader', $config) || !is_callable($config['typeLoader'])) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The typeLoader string is a constant of the PHP graphql library, we can const it but I don't think they expose a const for it (silly random associative arrays) |
||
return []; | ||
} | ||
|
||
return [ | ||
'typeLoader' => function ($typeName) use ($config) { | ||
$map = $this->builtInTypeMap(); | ||
if (array_key_exists($typeName, $map)) { | ||
return $map[$typeName]; | ||
} | ||
|
||
return $config['typeLoader']($typeName); | ||
} | ||
]; | ||
} | ||
|
||
private function builtInTypeMap(): array | ||
{ | ||
return [ | ||
EntityUnionType::getTypeName() => $this->entityUnionType, | ||
ServiceDefinitionType::getTypeName() => $this->serviceDefinitionType, | ||
AnyType::getTypeName() => $this->anyType | ||
]; | ||
} | ||
|
||
/** @var array */ | ||
private function getQueryTypeServiceFieldConfig(): array | ||
{ | ||
$serviceType = new ObjectType([ | ||
'name' => '_Service', | ||
'fields' => [ | ||
'sdl' => [ | ||
'type' => Type::string(), | ||
'resolve' => function () { | ||
return FederatedSchemaPrinter::doPrint($this); | ||
} | ||
] | ||
] | ||
]); | ||
|
||
return [ | ||
'_service' => [ | ||
'type' => Type::nonNull($serviceType), | ||
'type' => Type::nonNull($this->serviceDefinitionType), | ||
'resolve' => function () { | ||
return []; | ||
} | ||
|
@@ -149,28 +187,12 @@ private function getQueryTypeServiceFieldConfig(): array | |
/** @var array */ | ||
private function getQueryTypeEntitiesFieldConfig(?array $config): array | ||
{ | ||
if (!$this->hasEntityTypes()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is removed because it is incompatible with the notion of an invocable set of union types. I don't believe it is needed since the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reference you can link here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are various similar links throughout the readme, this is the one that specifies you have to include _entities |
||
return []; | ||
} | ||
|
||
$entityType = new UnionType([ | ||
'name' => '_Entity', | ||
'types' => array_values($this->getEntityTypes()) | ||
]); | ||
|
||
$anyType = new CustomScalarType([ | ||
'name' => '_Any', | ||
'serialize' => function ($value) { | ||
return $value; | ||
} | ||
]); | ||
|
||
return [ | ||
'_entities' => [ | ||
'type' => Type::listOf($entityType), | ||
'type' => Type::listOf($this->entityUnionType), | ||
'args' => [ | ||
'representations' => [ | ||
'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))) | ||
'type' => Type::nonNull(Type::listOf(Type::nonNull($this->anyType))) | ||
] | ||
], | ||
'resolve' => function ($root, $args, $context, $info) use ($config) { | ||
|
@@ -208,22 +230,25 @@ private function resolve($root, $args, $context, $info) | |
return $r; | ||
}, $args['representations']); | ||
} | ||
|
||
/** | ||
* @param array $config | ||
* | ||
* @return EntityObjectType[] | ||
* @return callable: EntityObjectType[] | ||
*/ | ||
private function extractEntityTypes(array $config): array | ||
private function lazyEntityTypeExtractor(array $config): callable | ||
{ | ||
$resolvedTypes = TypeInfo::extractTypes($config['query']); | ||
$entityTypes = []; | ||
|
||
foreach ($resolvedTypes as $type) { | ||
if ($type instanceof EntityObjectType) { | ||
$entityTypes[$type->name] = $type; | ||
return function () use ($config) { | ||
$resolvedTypes = TypeInfo::extractTypes($config['query']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More magic strings for the config, +1 for annotating that config array |
||
$entityTypes = []; | ||
|
||
foreach ($resolvedTypes as $type) { | ||
if ($type instanceof EntityObjectType) { | ||
$entityTypes[$type->name] = $type; | ||
} | ||
} | ||
} | ||
|
||
return $entityTypes; | ||
return $entityTypes; | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Apollo\Federation\Types; | ||
|
||
use GraphQL\Type\Definition\CustomScalarType; | ||
|
||
/** | ||
* Simple representation of an agnostic scalar value. | ||
*/ | ||
class AnyType extends CustomScalarType | ||
{ | ||
public function __construct() | ||
{ | ||
$config = [ | ||
'name' => self::getTypeName(), | ||
'serialize' => function ($value) { | ||
return $value; | ||
} | ||
]; | ||
parent::__construct($config); | ||
} | ||
|
||
public static function getTypeName(): string | ||
{ | ||
return '_Any'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,34 @@ | ||||||||
<?php | ||||||||
|
||||||||
declare(strict_types=1); | ||||||||
|
||||||||
namespace Apollo\Federation\Types; | ||||||||
|
||||||||
use GraphQL\Type\Definition\UnionType; | ||||||||
|
||||||||
/** | ||||||||
* The union of all entities defined within this schema. | ||||||||
*/ | ||||||||
class EntityUnionType extends UnionType | ||||||||
{ | ||||||||
|
||||||||
/** | ||||||||
* @param array|callable $entityTypes all entity types or a callable to retrieve them | ||||||||
*/ | ||||||||
public function __construct($entityTypes) | ||||||||
{ | ||||||||
$config = [ | ||||||||
'name' => self::getTypeName(), | ||||||||
'types' => is_callable($entityTypes) | ||||||||
? fn () => array_values($entityTypes()) | ||||||||
: array_values($entityTypes) | ||||||||
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Starting to see this pattern often enough. What we have is essential a type like function normalizeEntityTypes($maybeLazyEntityTypes): array {
return is_callable($maybeLazyEntityTypes)
? $maybeLazyEntityTypes()
: $maybeLazyEntityTypes;
} Which you can use as such:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to preserve the lazy nature of the operation So alas we cannot commonize this |
||||||||
|
||||||||
]; | ||||||||
parent::__construct($config); | ||||||||
} | ||||||||
|
||||||||
public static function getTypeName(): string | ||||||||
{ | ||||||||
return '_Entity'; | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Apollo\Federation\Types; | ||
|
||
use Apollo\Federation\Utils\FederatedSchemaPrinter; | ||
use GraphQL\Type\Definition\ObjectType; | ||
use GraphQL\Type\Definition\Type; | ||
use GraphQL\Type\Schema; | ||
|
||
/** | ||
* The type of the service definition required for federated schemas. | ||
*/ | ||
class ServiceDefinitionType extends ObjectType | ||
{ | ||
|
||
/** | ||
* @param Schema $schema - the schemas whose SDL should be printed. | ||
*/ | ||
public function __construct(Schema $schema) | ||
{ | ||
$config = [ | ||
'name' => self::getTypeName(), | ||
'fields' => [ | ||
'sdl' => [ | ||
'type' => Type::string(), | ||
'resolve' => fn () => FederatedSchemaPrinter::doPrint($schema) | ||
] | ||
] | ||
]; | ||
parent::__construct($config); | ||
} | ||
|
||
public static function getTypeName(): string | ||
{ | ||
return '_Service'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import ordering, this should be caught by linting