Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Merged
merged 4 commits into from
Apr 5, 2023
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 6 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
123 changes: 74 additions & 49 deletions 123 src/FederatedSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

namespace Apollo\Federation;

use Apollo\Federation\Types\AnyType;
Copy link
Contributor

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

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))
Expand Down Expand Up @@ -55,18 +55,36 @@
*/
class FederatedSchema extends Schema
Copy link
Contributor

Choose a reason for hiding this comment

The 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[] */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen the callable: ReturnType type declaration before- when was this introduced?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
Expand All @@ -78,7 +96,9 @@ public function __construct($config)
*/
public function getEntityTypes(): array
{
return $this->entityTypes;
return is_callable($this->entityTypes)
? ($this->entityTypes)()
: $this->entityTypes;
}

/**
Expand Down Expand Up @@ -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'])) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using the 'typeLoader' string a lot- we should share this value in a way that we understand the type signature of the config and expose that externally as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 [];
}
Expand All @@ -149,28 +187,12 @@ private function getQueryTypeServiceFieldConfig(): array
/** @var array */
private function getQueryTypeEntitiesFieldConfig(?array $config): array
{
if (!$this->hasEntityTypes()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 _entities query is always a required part of the subgraph definition

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reference you can link here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {
Expand Down Expand Up @@ -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']);
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
};
}
}
29 changes: 29 additions & 0 deletions 29 src/Types/AnyType.php
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';
}
}
34 changes: 34 additions & 0 deletions 34 src/Types/EntityUnionType.php
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
Copy link
Contributor

@tcarrio tcarrio Apr 5, 2023

Choose a reason for hiding this comment

The 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 MaybeLazyEntityTypes = T | () => T. We can define a common utility for working with this.

function normalizeEntityTypes($maybeLazyEntityTypes): array {
  return is_callable($maybeLazyEntityTypes)
        ? $maybeLazyEntityTypes()
        : $maybeLazyEntityTypes;
}

Which you can use as such:

Suggested change
'types' => is_callable($entityTypes)
? fn () => array_values($entityTypes())
: array_values($entityTypes)
'types' => array_values(normalizeEntityTypes($entityTypes)),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to preserve the lazy nature of the operation
So we need to wrap the operation in a new lazy operation that then updates it on invocation
Which is not the same as invoking it and operating on the result.

So alas we cannot commonize this


];
parent::__construct($config);
}

public static function getTypeName(): string
{
return '_Entity';
}
}
39 changes: 39 additions & 0 deletions 39 src/Types/ServiceDefinitionType.php
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';
}
}
10 changes: 8 additions & 2 deletions 10 test/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.