From aec360440b9f42309cd8574540bbc8a1b47c838b Mon Sep 17 00:00:00 2001 From: Joseph Pasque Date: Wed, 5 Apr 2023 11:25:11 -0500 Subject: [PATCH] feat: entity type customization, type loader support (#33) * feat: entity type customization, type loader support * feat: lazy union type loading * Update README.md Co-authored-by: Tom Carrio * fix: type docs updating --------- Co-authored-by: Tom Carrio --- README.md | 6 + src/FederatedSchema.php | 123 +++++++++++------- src/Types/AnyType.php | 29 +++++ src/Types/EntityUnionType.php | 34 +++++ src/Types/ServiceDefinitionType.php | 39 ++++++ test/SchemaTest.php | 10 +- test/StarWarsSchema.php | 64 ++++++++- ...t__testSchemaSdlForProvidedEntities__1.txt | 42 ++++++ 8 files changed, 291 insertions(+), 56 deletions(-) create mode 100644 src/Types/AnyType.php create mode 100644 src/Types/EntityUnionType.php create mode 100644 src/Types/ServiceDefinitionType.php create mode 100644 test/__snapshots__/SchemaTest__testSchemaSdlForProvidedEntities__1.txt diff --git a/README.md b/README.md index cb391f7..347a250 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,12 @@ $query = 'query GetServiceSDL { _service { sdl } }'; $result = GraphQL::executeQuery($schema, $query); ``` +#### Config + +The config parameter for the `FederatedSchema` object is entirely compatible with the `Schema` config argument. On top of this, we support the following optional parameters: + +1. `entityTypes` - the entity types (which extend `EntityObjectType`) which will form the `_Entity` type on the federated schema. If not provided, the Schema will scan the `Query` type tree for all types extending `EntityObjectType`. + ## Disclaimer Documentation in this project include content quoted directly from the [Apollo official documentation](https://www.apollographql.com/docs) to reduce redundancy. \ No newline at end of file diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index af3682b..de6e754 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -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 { - /** @var EntityObjectType[] */ + /** @var EntityObjectType[]|callable: EntityObjectType[] */ 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, ?typeLoader: callable, query: array} $config + */ public function __construct($config) { - $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'])) { + 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()) { - 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']); + $entityTypes = []; + + foreach ($resolvedTypes as $type) { + if ($type instanceof EntityObjectType) { + $entityTypes[$type->name] = $type; + } } - } - return $entityTypes; + return $entityTypes; + }; } } diff --git a/src/Types/AnyType.php b/src/Types/AnyType.php new file mode 100644 index 0000000..13edc53 --- /dev/null +++ b/src/Types/AnyType.php @@ -0,0 +1,29 @@ + self::getTypeName(), + 'serialize' => function ($value) { + return $value; + } + ]; + parent::__construct($config); + } + + public static function getTypeName(): string + { + return '_Any'; + } +} diff --git a/src/Types/EntityUnionType.php b/src/Types/EntityUnionType.php new file mode 100644 index 0000000..5877bdb --- /dev/null +++ b/src/Types/EntityUnionType.php @@ -0,0 +1,34 @@ + self::getTypeName(), + 'types' => is_callable($entityTypes) + ? fn () => array_values($entityTypes()) + : array_values($entityTypes) + + ]; + parent::__construct($config); + } + + public static function getTypeName(): string + { + return '_Entity'; + } +} diff --git a/src/Types/ServiceDefinitionType.php b/src/Types/ServiceDefinitionType.php new file mode 100644 index 0000000..1abbca5 --- /dev/null +++ b/src/Types/ServiceDefinitionType.php @@ -0,0 +1,39 @@ + self::getTypeName(), + 'fields' => [ + 'sdl' => [ + 'type' => Type::string(), + 'resolve' => fn () => FederatedSchemaPrinter::doPrint($schema) + ] + ] + ]; + parent::__construct($config); + } + + public static function getTypeName(): string + { + return '_Service'; + } +} diff --git a/test/SchemaTest.php b/test/SchemaTest.php index f41fefc..474e8e4 100644 --- a/test/SchemaTest.php +++ b/test/SchemaTest.php @@ -8,11 +8,9 @@ use Spatie\Snapshots\MatchesSnapshots; use GraphQL\GraphQL; -use GraphQL\Type\Definition\Type; use GraphQL\Utils\SchemaPrinter; use Apollo\Federation\Tests\StarWarsSchema; -use Apollo\Federation\Tests\DungeonsAndDragonsSchema; class SchemaTest extends TestCase { @@ -88,6 +86,14 @@ public function testSchemaSdl() $this->assertMatchesSnapshot($schemaSdl); } + public function testSchemaSdlForProvidedEntities() + { + $schema = StarWarsSchema::getEpisodesSchemaWithProvidedEntities(); + $schemaSdl = SchemaPrinter::doPrint($schema); + + $this->assertMatchesSnapshot($schemaSdl); + } + public function testResolvingEntityReferences() { $schema = StarWarsSchema::getEpisodesSchema(); diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 5594896..bcba482 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -16,11 +16,32 @@ class StarWarsSchema public static $episodesSchema; public static $overRiddedEpisodesSchema; + public static $episodeType = null; + public static $locationType = null; + public static $characterType = null; + public static function getEpisodesSchema(): FederatedSchema { if (!self::$episodesSchema) { self::$episodesSchema = new FederatedSchema([ - 'query' => self::getQueryType() + 'query' => self::getQueryType(), + 'typeLoader' => self::getTypeLoader() + ]); + } + return self::$episodesSchema; + } + + public static function getEpisodesSchemaWithProvidedEntities(): FederatedSchema + { + if (!self::$episodesSchema) { + self::$episodesSchema = new FederatedSchema([ + 'query' => self::getQueryType(), + 'typeLoader' => self::getTypeLoader(), + 'entityTypes' => [ + self::getCharacterType(), + self::getLocationType(), + self::getEpisodeType() + ] ]); } return self::$episodesSchema; @@ -38,12 +59,27 @@ public static function getEpisodesSchemaCustomResolver(): FederatedSchema $ref["id"] = $ref["id"] + 1; return $type->resolveReference($ref); }, $args['representations']); - } + }, + 'typeLoader' => self::getTypeLoader() ]); } return self::$overRiddedEpisodesSchema; } + public static function getTypeLoader(): callable { + return function ($typeName) { + if ($typeName === "Episode") { + return self::getEpisodeType(); + } + else if ($typeName === "Character") { + return self::getCharacterType(); + } + else if ($typeName === "Location") { + return self::getLocationType(); + } + }; + } + private static function getQueryType(): ObjectType { $episodeType = self::getEpisodeType(); @@ -68,7 +104,11 @@ private static function getQueryType(): ObjectType private static function getEpisodeType(): EntityObjectType { - return new EntityObjectType([ + if (self::$episodeType) { + return self::$episodeType; + } + + self::$episodeType = new EntityObjectType([ 'name' => 'Episode', 'description' => 'A film in the Star Wars Trilogy', 'fields' => [ @@ -94,11 +134,17 @@ private static function getEpisodeType(): EntityObjectType return $entity; } ]); + + return self::$episodeType; } private static function getCharacterType(): EntityRefObjectType { - return new EntityRefObjectType([ + if (self::$characterType) { + return self::$characterType; + } + + self::$characterType = new EntityRefObjectType([ 'name' => 'Character', 'description' => 'A character in the Star Wars Trilogy', 'fields' => [ @@ -120,11 +166,17 @@ private static function getCharacterType(): EntityRefObjectType ], 'keyFields' => ['id'] ]); + + return self::$characterType; } private static function getLocationType(): EntityRefObjectType { - return new EntityRefObjectType([ + if (self::$locationType) { + return self::$locationType; + } + + self::$locationType = new EntityRefObjectType([ 'name' => 'Location', 'description' => 'A location in the Star Wars Trilogy', 'fields' => [ @@ -139,5 +191,7 @@ private static function getLocationType(): EntityRefObjectType ], 'keyFields' => ['id'] ]); + + return self::$locationType; } } diff --git a/test/__snapshots__/SchemaTest__testSchemaSdlForProvidedEntities__1.txt b/test/__snapshots__/SchemaTest__testSchemaSdlForProvidedEntities__1.txt new file mode 100644 index 0000000..fd4a936 --- /dev/null +++ b/test/__snapshots__/SchemaTest__testSchemaSdlForProvidedEntities__1.txt @@ -0,0 +1,42 @@ +directive @key(fields: String!) on OBJECT | INTERFACE + +directive @external on FIELD_DEFINITION + +directive @requires(fields: String!) on FIELD_DEFINITION + +directive @provides(fields: String!) on FIELD_DEFINITION + +"""A character in the Star Wars Trilogy""" +type Character { + id: Int! + name: String! + locations: [Location]! +} + +"""A film in the Star Wars Trilogy""" +type Episode { + id: Int! + title: String! + characters: [Character!]! +} + +"""A location in the Star Wars Trilogy""" +type Location { + id: Int! + name: String! +} + +type Query { + episodes: [Episode!]! + deprecatedEpisodes: [Episode!]! @deprecated(reason: "Because you should use the other one.") + _service: _Service! + _entities(representations: [_Any!]!): [_Entity] +} + +scalar _Any + +union _Entity = Episode | Character | Location + +type _Service { + sdl: String +}