new Deps
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use Nuwave\Lighthouse\Events\ManipulateAST;
|
||||
use Nuwave\Lighthouse\Exceptions\FederationException;
|
||||
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
|
||||
|
||||
class ASTManipulator
|
||||
{
|
||||
public function handle(ManipulateAST $manipulateAST): void
|
||||
{
|
||||
$documentAST = $manipulateAST->documentAST;
|
||||
|
||||
$this->addScalars($documentAST);
|
||||
$this->addEntityUnion($documentAST);
|
||||
$this->addRootFields($documentAST);
|
||||
$this->addServiceType($documentAST);
|
||||
}
|
||||
|
||||
protected function addScalars(DocumentAST &$documentAST): void
|
||||
{
|
||||
$documentAST->setTypeDefinition(
|
||||
Parser::scalarTypeDefinition(/** @lang GraphQL */ '
|
||||
scalar _Any @scalar(class: "Nuwave\\\Lighthouse\\\Federation\\\Types\\\Any")
|
||||
')
|
||||
);
|
||||
|
||||
$documentAST->setTypeDefinition(
|
||||
Parser::scalarTypeDefinition(/** @lang GraphQL */ '
|
||||
scalar _FieldSet @scalar(class: "Nuwave\\\Lighthouse\\\Federation\\\Types\\\FieldSet")
|
||||
')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine object types with `@key` into the _Entity union.
|
||||
*
|
||||
* @throws \Nuwave\Lighthouse\Exceptions\FederationException
|
||||
*/
|
||||
protected function addEntityUnion(DocumentAST &$documentAST): void
|
||||
{
|
||||
/** @var array<int, string> $entities */
|
||||
$entities = [];
|
||||
|
||||
foreach ($documentAST->types as $type) {
|
||||
if (! $type instanceof ObjectTypeDefinitionNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \GraphQL\Language\AST\DirectiveNode $directive */
|
||||
foreach ($type->directives as $directive) {
|
||||
if ($directive->name->value === 'key') {
|
||||
$entities[] = $type->name->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($entities) === 0) {
|
||||
throw new FederationException('There must be at least one type using the @key directive when federation is enabled.');
|
||||
}
|
||||
|
||||
$entitiesString = implode(' | ', $entities);
|
||||
$documentAST->setTypeDefinition(
|
||||
Parser::unionTypeDefinition(/** @lang GraphQL */ "
|
||||
union _Entity = {$entitiesString}
|
||||
")
|
||||
);
|
||||
}
|
||||
|
||||
protected function addRootFields(DocumentAST &$documentAST): void
|
||||
{
|
||||
/** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode $queryType */
|
||||
$queryType = $documentAST->types['Query'];
|
||||
|
||||
$queryType->fields [] = Parser::fieldDefinition(/** @lang GraphQL */ '
|
||||
_entities(
|
||||
representations: [_Any!]!
|
||||
): [_Entity]! @field(resolver: "Nuwave\\\Lighthouse\\\Federation\\\Resolvers\\\Entities")
|
||||
');
|
||||
|
||||
$queryType->fields [] = Parser::fieldDefinition(/** @lang GraphQL */ '
|
||||
_service: _Service! @field(resolver: "Nuwave\\\Lighthouse\\\Federation\\\Resolvers\\\Service")
|
||||
');
|
||||
}
|
||||
|
||||
protected function addServiceType(DocumentAST &$documentAST): void
|
||||
{
|
||||
$documentAST->setTypeDefinition(
|
||||
Parser::objectTypeDefinition(/** @lang GraphQL */ '
|
||||
type _Service {
|
||||
sdl: String
|
||||
}
|
||||
')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Directives;
|
||||
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
|
||||
class ExtendsDirective extends BaseDirective
|
||||
{
|
||||
const NAME = 'extends';
|
||||
|
||||
/**
|
||||
* @see https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#schema-modifications-glossary
|
||||
*/
|
||||
public static function definition(): string
|
||||
{
|
||||
return /* @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Some libraries such as graphql-java don't have native support for type extensions in their printer. Apollo Federation
|
||||
supports using an @extends directive in place of extend type to annotate type references:
|
||||
|
||||
type User @key(fields: "id") @extends {
|
||||
|
||||
instead of:
|
||||
|
||||
extend type User @key(fields: "id") {
|
||||
"""
|
||||
directive @extends on OBJECT | INTERFACE
|
||||
GRAPHQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Directives;
|
||||
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
use Nuwave\Lighthouse\Schema\Values\FieldValue;
|
||||
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
|
||||
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
class ExternalDirective extends BaseDirective implements FieldResolver
|
||||
{
|
||||
const NAME = 'external';
|
||||
|
||||
public static function definition(): string
|
||||
{
|
||||
return /* @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Individual federated services should be runnable without having the entire graph present. Fields marked with @external
|
||||
are declarations of fields that are defined in another service. All fields referred to in @key, @requires, and @provides
|
||||
directives need to have corresponding @external fields in the same service.
|
||||
|
||||
https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#schema-modifications-glossary
|
||||
"""
|
||||
directive @external on FIELD_DEFINITION
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
public function resolveField(FieldValue $fieldValue): FieldValue
|
||||
{
|
||||
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) {
|
||||
// The parent might just hold a foreign key to the external object, in which case we just return that.
|
||||
return is_scalar($root)
|
||||
? $root
|
||||
: (Executor::getDefaultFieldResolver())($root, $args, $context, $resolveInfo);
|
||||
});
|
||||
|
||||
return $fieldValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Directives;
|
||||
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use Nuwave\Lighthouse\Exceptions\DefinitionException;
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
|
||||
class KeyDirective extends BaseDirective
|
||||
{
|
||||
const NAME = 'key';
|
||||
|
||||
/**
|
||||
* @see https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#schema-modifications-glossary
|
||||
*/
|
||||
public static function definition(): string
|
||||
{
|
||||
return /* @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
The @key directive is used to indicate a combination of fields that
|
||||
can be used to uniquely identify and fetch an object or interface.
|
||||
"""
|
||||
directive @key(
|
||||
"""
|
||||
Fields that can be used to uniquely identify and fetch an object or interface.
|
||||
"""
|
||||
fields: _FieldSet!
|
||||
) repeatable on OBJECT | INTERFACE
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
public function fields(): SelectionSetNode
|
||||
{
|
||||
$fields = $this->directiveArgValue('fields');
|
||||
if (! is_string($fields)) {
|
||||
throw new DefinitionException('Argument `fields` on the `@key` directive is required.');
|
||||
}
|
||||
|
||||
// Grammatically, a field set is a selection set minus the braces.
|
||||
// https://www.apollographql.com/docs/federation/federation-spec/#scalar-_fieldset
|
||||
return Parser::selectionSet("{ {$fields} }");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Directives;
|
||||
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
|
||||
class ProvidesDirective extends BaseDirective
|
||||
{
|
||||
const NAME = 'provides';
|
||||
|
||||
public static function definition(): string
|
||||
{
|
||||
return /* @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
The `@provides` directive is used to annotate the expected returned fieldset from a field
|
||||
on a base type that is guaranteed to be selectable by the gateway. Given the following example:
|
||||
|
||||
```graphql
|
||||
type Review @key(fields: "id") {
|
||||
product: Product @provides(fields: "name")
|
||||
}
|
||||
|
||||
extend type Product @key(fields: "upc") {
|
||||
upc: String @external
|
||||
name: String @external
|
||||
}
|
||||
```
|
||||
|
||||
When fetching `Review.product` from the Reviews service, it is possible to request the `name`
|
||||
with the expectation that the Reviews service can provide it when going from review to product.
|
||||
`Product.name` is an external field on an external type which is why the local type extension
|
||||
of `Product` and annotation of `name` is required.
|
||||
|
||||
https://www.apollographql.com/docs/federation/federation-spec/#provides
|
||||
"""
|
||||
directive @provides(
|
||||
"""
|
||||
The fields this service can provide from the returned type of this field.
|
||||
"""
|
||||
fields: _FieldSet!
|
||||
) on FIELD_DEFINITION
|
||||
GRAPHQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Directives;
|
||||
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
|
||||
class RequiresDirective extends BaseDirective
|
||||
{
|
||||
const NAME = 'requires';
|
||||
|
||||
/**
|
||||
* @see https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#schema-modifications-glossary
|
||||
*/
|
||||
public static function definition(): string
|
||||
{
|
||||
return /* @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
The @requires directive is used to annotate the required input fieldset from a base type for a resolver. It is used to
|
||||
develop a query plan where the required fields may not be needed by the client, but the service may need additional
|
||||
information from other services. For example:
|
||||
|
||||
extend type User @key(fields: "id") {
|
||||
id: ID! @external
|
||||
email: String @external
|
||||
reviews: [Review] @requires(fields: "email")
|
||||
}
|
||||
"""
|
||||
directive @requires(
|
||||
"""
|
||||
It is used to develop a query plan where the required fields may not be needed by the client, but the service may
|
||||
need additional information from other services.
|
||||
"""
|
||||
fields: _FieldSet!
|
||||
) on FIELD_DEFINITION
|
||||
GRAPHQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use Closure;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Nuwave\Lighthouse\Exceptions\DefinitionException;
|
||||
use Nuwave\Lighthouse\Exceptions\FederationException;
|
||||
use Nuwave\Lighthouse\Federation\Directives\KeyDirective;
|
||||
use Nuwave\Lighthouse\Schema\DirectiveLocator;
|
||||
use Nuwave\Lighthouse\Schema\Directives\ModelDirective;
|
||||
use Nuwave\Lighthouse\Schema\SchemaBuilder;
|
||||
use Nuwave\Lighthouse\Support\Utils;
|
||||
|
||||
class EntityResolverProvider
|
||||
{
|
||||
/**
|
||||
* @var \GraphQL\Type\Schema
|
||||
*/
|
||||
protected $schema;
|
||||
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Schema\DirectiveLocator
|
||||
*/
|
||||
protected $directiveLocator;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
protected $configRepository;
|
||||
|
||||
/**
|
||||
* Maps from __typename to definitions.
|
||||
*
|
||||
* @var array<string, \GraphQL\Language\AST\ObjectTypeDefinitionNode>
|
||||
*/
|
||||
protected $definitions;
|
||||
|
||||
/**
|
||||
* Maps from __typename to resolver.
|
||||
*
|
||||
* @var array<string, \Closure(array<string, mixed>): mixed>
|
||||
*/
|
||||
protected $resolvers;
|
||||
|
||||
public function __construct(SchemaBuilder $schemaBuilder, DirectiveLocator $directiveLocator, ConfigRepository $configRepository)
|
||||
{
|
||||
$this->schema = $schemaBuilder->schema();
|
||||
$this->directiveLocator = $directiveLocator;
|
||||
$this->configRepository = $configRepository;
|
||||
}
|
||||
|
||||
public static function missingResolver(string $typename): string
|
||||
{
|
||||
return "Could not locate a resolver for __typename `{$typename}`.";
|
||||
}
|
||||
|
||||
public static function unknownTypename(string $typename): string
|
||||
{
|
||||
return "Unknown __typename `{$typename}`.";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Closure(array<string, mixed> $representations): mixed
|
||||
*/
|
||||
public function resolver(string $typename): Closure
|
||||
{
|
||||
if (isset($this->resolvers[$typename])) {
|
||||
return $this->resolvers[$typename];
|
||||
}
|
||||
|
||||
$resolver = $this->resolverFromClass($typename)
|
||||
?? $this->resolverFromModel($typename)
|
||||
?? null;
|
||||
|
||||
if ($resolver === null) {
|
||||
throw new Error(self::missingResolver($typename));
|
||||
}
|
||||
|
||||
$this->resolvers[$typename] = $resolver;
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
public function typeDefinition(string $typename): ObjectTypeDefinitionNode
|
||||
{
|
||||
if (isset($this->definitions[$typename])) {
|
||||
return $this->definitions[$typename];
|
||||
}
|
||||
|
||||
$type = null;
|
||||
try {
|
||||
$type = $this->schema->getType($typename);
|
||||
} catch (DefinitionException $definitionException) {
|
||||
// Signalizes the type is unknown, handled by the null check below
|
||||
}
|
||||
if ($type === null) {
|
||||
throw new Error(self::unknownTypename($typename));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO remove when upgrading graphql-php.
|
||||
*
|
||||
* @var (\GraphQL\Language\AST\Node&\GraphQL\Language\AST\TypeDefinitionNode)|null $definition
|
||||
*/
|
||||
$definition = $type->astNode;
|
||||
if ($definition === null) {
|
||||
throw new FederationException("Must provide AST definition for type `{$typename}`.");
|
||||
}
|
||||
|
||||
if (! $definition instanceof ObjectTypeDefinitionNode) {
|
||||
throw new Error("Expected __typename `{$typename}` to be ObjectTypeDefinition, got {$definition->kind}.");
|
||||
}
|
||||
|
||||
$this->definitions[$typename] = $definition;
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
protected function resolverFromClass(string $typename): ?Closure
|
||||
{
|
||||
$resolverClass = Utils::namespaceClassname(
|
||||
$typename,
|
||||
(array) config('lighthouse.federation.entities_resolver_namespace'),
|
||||
'class_exists'
|
||||
);
|
||||
|
||||
if ($resolverClass === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Utils::constructResolver($resolverClass, '__invoke');
|
||||
}
|
||||
|
||||
protected function resolverFromModel(string $typeName): ?Closure
|
||||
{
|
||||
$definition = $this->typeDefinition($typeName);
|
||||
|
||||
$model = ModelDirective::modelClass($definition) ?? $typeName;
|
||||
|
||||
/** @var class-string<\Illuminate\Database\Eloquent\Model>|null $modelClass */
|
||||
$modelClass = Utils::namespaceClassname(
|
||||
$model,
|
||||
(array) $this->configRepository->get('lighthouse.namespaces.models'),
|
||||
static function (string $classCandidate): bool {
|
||||
return is_subclass_of($classCandidate, Model::class);
|
||||
}
|
||||
);
|
||||
if ($modelClass === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keyFieldsSelections = $this->keyFieldsSelections($definition);
|
||||
|
||||
return function (array $representation) use ($keyFieldsSelections, $modelClass): ?Model {
|
||||
/** @var \Illuminate\Database\Eloquent\Builder $builder */
|
||||
$builder = $modelClass::query();
|
||||
$this->constrainKeys($builder, $keyFieldsSelections, $representation);
|
||||
|
||||
$results = $builder->get();
|
||||
if ($results->count() > 1) {
|
||||
throw new Error('The query returned more than one result.');
|
||||
}
|
||||
|
||||
return $results->first();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<\GraphQL\Language\AST\SelectionSetNode> $keyFieldsSelections
|
||||
* @param array<string, mixed> $representation
|
||||
*/
|
||||
protected function constrainKeys(Builder $builder, Collection $keyFieldsSelections, array $representation): void
|
||||
{
|
||||
$this->applySatisfiedSelection(
|
||||
$builder,
|
||||
$this->firstSatisfiedKeyFields($keyFieldsSelections, $representation),
|
||||
$representation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $representation
|
||||
*/
|
||||
protected function satisfiesKeyFields(SelectionSetNode $keyFields, array $representation): bool
|
||||
{
|
||||
/**
|
||||
* Fragments or spreads are not allowed in key fields.
|
||||
* @see \Nuwave\Lighthouse\Federation\SchemaValidator
|
||||
*
|
||||
* @var \GraphQL\Language\AST\FieldNode $field
|
||||
*/
|
||||
foreach ($keyFields->selections as $field) {
|
||||
$fieldName = $field->name->value;
|
||||
$value = $representation[$fieldName] ?? null;
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subSelection = $field->selectionSet;
|
||||
if ($subSelection !== null) {
|
||||
if (! is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subSelectionProvidesKeys = $this->satisfiesKeyFields($subSelection, $value);
|
||||
if (! $subSelectionProvidesKeys) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $representation
|
||||
*/
|
||||
protected function applySatisfiedSelection(Builder $builder, SelectionSetNode $keyFields, array $representation): void
|
||||
{
|
||||
/**
|
||||
* Fragments or spreads are not allowed in key fields.
|
||||
*
|
||||
* @var \GraphQL\Language\AST\FieldNode $field
|
||||
*/
|
||||
foreach ($keyFields->selections as $field) {
|
||||
$fieldName = $field->name->value;
|
||||
$value = $representation[$fieldName];
|
||||
|
||||
$subSelection = $field->selectionSet;
|
||||
if ($subSelection === null) {
|
||||
$builder->where($fieldName, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applySatisfiedSelection($builder, $subSelection, $representation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<\GraphQL\Language\AST\SelectionSetNode>
|
||||
*/
|
||||
public function keyFieldsSelections(ObjectTypeDefinitionNode $definition): Collection
|
||||
{
|
||||
return $this->directiveLocator
|
||||
->associatedOfType($definition, KeyDirective::class)
|
||||
->map(static function (KeyDirective $keyDirective): SelectionSetNode {
|
||||
return $keyDirective->fields();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<\GraphQL\Language\AST\SelectionSetNode> $keyFieldsSelections
|
||||
* @param array<string, mixed> $representation
|
||||
*/
|
||||
public function firstSatisfiedKeyFields(Collection $keyFieldsSelections, array $representation): SelectionSetNode
|
||||
{
|
||||
$satisfiedKeyFields = $keyFieldsSelections->first(
|
||||
function (SelectionSetNode $keyFields) use ($representation): bool {
|
||||
return $this->satisfiesKeyFields($keyFields, $representation);
|
||||
}
|
||||
);
|
||||
|
||||
if ($satisfiedKeyFields === null) {
|
||||
throw new Error('Representation does not satisfy any set of uniquely identifying keys: '.\Safe\json_encode($representation));
|
||||
}
|
||||
|
||||
return $satisfiedKeyFields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectField;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\SchemaConfig;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Federation\Directives\ExtendsDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\ExternalDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\KeyDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\ProvidesDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\RequiresDirective;
|
||||
|
||||
class FederationPrinter
|
||||
{
|
||||
const FEDERATION_TYPES = [
|
||||
'_Any',
|
||||
'_Entity',
|
||||
'_FieldSet',
|
||||
'_Service',
|
||||
];
|
||||
|
||||
const FEDERATION_FIELDS = [
|
||||
'_service',
|
||||
'_entities',
|
||||
];
|
||||
|
||||
const FEDERATION_DIRECTIVES = [
|
||||
ExtendsDirective::NAME,
|
||||
ExternalDirective::NAME,
|
||||
KeyDirective::NAME,
|
||||
ProvidesDirective::NAME,
|
||||
RequiresDirective::NAME,
|
||||
];
|
||||
|
||||
public static function print(Schema $schema): string
|
||||
{
|
||||
$config = SchemaConfig::create();
|
||||
|
||||
$types = $schema->getTypeMap();
|
||||
foreach (self::FEDERATION_TYPES as $type) {
|
||||
unset($types[$type]);
|
||||
}
|
||||
|
||||
$originalQueryType = Arr::pull($types, 'Query');
|
||||
$config->setQuery(new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => array_filter(
|
||||
$originalQueryType->getFields(),
|
||||
static function (FieldDefinition $field): bool {
|
||||
return ! in_array($field->name, static::FEDERATION_FIELDS);
|
||||
}
|
||||
),
|
||||
'interfaces' => $originalQueryType->getInterfaces(),
|
||||
]));
|
||||
|
||||
$config->setMutation(Arr::pull($types, 'Mutation'));
|
||||
|
||||
$config->setSubscription(Arr::pull($types, 'Subscription'));
|
||||
|
||||
$config->setTypes($types);
|
||||
|
||||
$config->setDirectives(array_filter(
|
||||
$schema->getDirectives(),
|
||||
static function (Directive $directive): bool {
|
||||
return ! in_array($directive->name, static::FEDERATION_DIRECTIVES);
|
||||
}
|
||||
));
|
||||
|
||||
$printDirectives = static function ($definition): string {
|
||||
/** @var Type|EnumValueDefinition|FieldArgument|FieldDefinition|InputObjectField $definition */
|
||||
$astNode = $definition->astNode;
|
||||
if ($astNode === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($astNode instanceof ObjectTypeDefinitionNode) {
|
||||
return SchemaPrinter::printDirectives(
|
||||
Utils::filter(
|
||||
$astNode->directives,
|
||||
static function (DirectiveNode $directive): bool {
|
||||
$name = $directive->name->value;
|
||||
|
||||
return $name === KeyDirective::NAME
|
||||
|| $name === ExtendsDirective::NAME;
|
||||
}
|
||||
)
|
||||
);
|
||||
} elseif ($astNode instanceof FieldDefinitionNode) {
|
||||
return SchemaPrinter::printDirectives(
|
||||
Utils::filter(
|
||||
$astNode->directives,
|
||||
static function (DirectiveNode $directive): bool {
|
||||
$name = $directive->name->value;
|
||||
|
||||
return $name === ProvidesDirective::NAME
|
||||
|| $name === RequiresDirective::NAME
|
||||
|| $name === ExternalDirective::NAME;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
return SchemaPrinter::doPrint(
|
||||
new Schema($config),
|
||||
// @phpstan-ignore-next-line We extended the SchemaPrinter to allow for this option
|
||||
['printDirectives' => $printDirectives]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nuwave\Lighthouse\Events\ManipulateAST;
|
||||
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
|
||||
use Nuwave\Lighthouse\Events\ValidateSchema;
|
||||
|
||||
class FederationServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(EntityResolverProvider::class);
|
||||
}
|
||||
|
||||
public function boot(EventsDispatcher $eventsDispatcher): void
|
||||
{
|
||||
$eventsDispatcher->listen(
|
||||
RegisterDirectiveNamespaces::class,
|
||||
static function (): string {
|
||||
return __NAMESPACE__.'\\Directives';
|
||||
}
|
||||
);
|
||||
|
||||
$eventsDispatcher->listen(ManipulateAST::class, ASTManipulator::class);
|
||||
$eventsDispatcher->listen(ValidateSchema::class, SchemaValidator::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Resolvers;
|
||||
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Nuwave\Lighthouse\Federation\EntityResolverProvider;
|
||||
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
/**
|
||||
* Resolver for the _entities field.
|
||||
*
|
||||
* @see https://www.apollographql.com/docs/federation/federation-spec/#resolve-requests-for-entities
|
||||
*/
|
||||
class Entities
|
||||
{
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Federation\EntityResolverProvider
|
||||
*/
|
||||
protected $entityResolverProvider;
|
||||
|
||||
public function __construct(EntityResolverProvider $entityResolverProvider)
|
||||
{
|
||||
$this->entityResolverProvider = $entityResolverProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{representations: array<int, mixed>} $args
|
||||
* @return list<mixed>
|
||||
*/
|
||||
public function __invoke($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($args['representations'] as $representation) {
|
||||
$typename = $representation['__typename'];
|
||||
$resolver = $this->entityResolverProvider->resolver($typename);
|
||||
|
||||
$results [] = $resolver($representation);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Resolvers;
|
||||
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Nuwave\Lighthouse\Federation\FederationPrinter;
|
||||
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $args Always empty
|
||||
* @return array{sdl: string}
|
||||
*/
|
||||
public function __invoke($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): array
|
||||
{
|
||||
return [
|
||||
'sdl' => FederationPrinter::print($resolveInfo->schema),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\SchemaPrinter as GraphQLSchemaPrinter;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class SchemaPrinter extends GraphQLSchemaPrinter
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @param \GraphQL\Type\Definition\ObjectType|\GraphQL\Type\Definition\InterfaceType $type
|
||||
*/
|
||||
protected static function printFields(array $options, $type): string
|
||||
{
|
||||
$firstInBlock = true;
|
||||
|
||||
return implode(
|
||||
"\n",
|
||||
array_map(
|
||||
static function (FieldDefinition $f) use (&$firstInBlock, $options): string {
|
||||
$description = static::printDescription($options, $f, ' ', $firstInBlock)
|
||||
.' '
|
||||
.$f->name
|
||||
.static::printArgs($options, $f->args, ' ')
|
||||
.': '
|
||||
.(string) $f->getType()
|
||||
.(isset($options['printDirectives'])
|
||||
? $options['printDirectives']($f)
|
||||
: '')
|
||||
.static::printDeprecated($f);
|
||||
|
||||
$firstInBlock = false;
|
||||
|
||||
return $description;
|
||||
},
|
||||
$type->getFields()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
protected static function printObject(ObjectType $type, array $options): string
|
||||
{
|
||||
return static::printObjectLike('type', $type, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
protected static function printInterface(InterfaceType $type, array $options): string
|
||||
{
|
||||
return static::printObjectLike('interface', $type, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \GraphQL\Type\Definition\ObjectType|\GraphQL\Type\Definition\InterfaceType $type
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
protected static function printObjectLike(string $kind, Type $type, array $options): string
|
||||
{
|
||||
$interfaces = $type->getInterfaces();
|
||||
$implementedInterfaces = count($interfaces) > 0
|
||||
? ' implements '.implode(
|
||||
' & ',
|
||||
array_map(
|
||||
static function (InterfaceType $interface): string {
|
||||
return $interface->name;
|
||||
},
|
||||
$interfaces
|
||||
)
|
||||
)
|
||||
: '';
|
||||
|
||||
$description = static::printDescription($options, $type);
|
||||
$directives = isset($options['printDirectives'])
|
||||
? $options['printDirectives']($type)
|
||||
: '';
|
||||
$fields = static::printFields($options, $type);
|
||||
|
||||
return <<<GRAPHQL
|
||||
{$description}{$kind} {$type->name}{$implementedInterfaces}{$directives} {
|
||||
{$fields}
|
||||
}
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<\GraphQL\Language\AST\DirectiveNode> $directives
|
||||
*/
|
||||
public static function printDirectives(array $directives): string
|
||||
{
|
||||
if (count($directives) === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' '
|
||||
.implode(
|
||||
' ',
|
||||
Utils::map($directives, static function (DirectiveNode $directive): string {
|
||||
return Printer::doPrint($directive);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use Nuwave\Lighthouse\Events\ValidateSchema;
|
||||
use Nuwave\Lighthouse\Exceptions\FederationException;
|
||||
use Nuwave\Lighthouse\Federation\Directives\ExtendsDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\ExternalDirective;
|
||||
use Nuwave\Lighthouse\Federation\Directives\KeyDirective;
|
||||
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
|
||||
use Nuwave\Lighthouse\Schema\DirectiveLocator;
|
||||
|
||||
class SchemaValidator
|
||||
{
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Schema\DirectiveLocator
|
||||
*/
|
||||
protected $directiveLocator;
|
||||
|
||||
public function __construct(DirectiveLocator $directiveLocator)
|
||||
{
|
||||
$this->directiveLocator = $directiveLocator;
|
||||
}
|
||||
|
||||
public function handle(ValidateSchema $validateSchema): void
|
||||
{
|
||||
$schema = $validateSchema->schema;
|
||||
|
||||
foreach ($schema->getTypeMap() as $type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
$this->validateObjectType($type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateObjectType(ObjectType $type): void
|
||||
{
|
||||
$ast = $type->astNode;
|
||||
if ($ast !== null) {
|
||||
$directives = $this->directiveLocator->associated($ast);
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Support\Contracts\Directive $directive */
|
||||
foreach ($directives as $directive) {
|
||||
if ($directive instanceof KeyDirective) {
|
||||
$this->validateKeySelectionSet($directive->fields(), $type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Nuwave\Lighthouse\Exceptions\FederationException
|
||||
*/
|
||||
protected function validateKeySelectionSet(SelectionSetNode $selectionSet, ObjectType $type): void
|
||||
{
|
||||
foreach ($selectionSet->selections as $selection) {
|
||||
if (! $selection instanceof FieldNode) {
|
||||
throw new FederationException("Must only use field selections in the `fields` argument of @key, got: {$selection->kind}.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Throws if the field is not defined
|
||||
$field = $type->getField($selection->name->value);
|
||||
} catch (InvariantViolation $i) {
|
||||
throw new FederationException($i->getMessage(), $i->getCode(), $i);
|
||||
}
|
||||
|
||||
if (
|
||||
ASTHelper::hasDirective($type->astNode, ExtendsDirective::NAME)
|
||||
&& ! ASTHelper::hasDirective($field->astNode, ExternalDirective::NAME)
|
||||
) {
|
||||
throw new FederationException("A @key directive on `{$type->name}` specifies the `{$field->name}` field which has no @external directive.");
|
||||
}
|
||||
|
||||
$nestedSelection = $selection->selectionSet;
|
||||
if ($nestedSelection !== null) {
|
||||
$type = $field->getType();
|
||||
if ($type instanceof WrappingType) {
|
||||
$type = $type->getWrappedType(true);
|
||||
}
|
||||
|
||||
$this->validateKeySelectionSet($nestedSelection, $type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Types;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Utils\AST;
|
||||
use Nuwave\Lighthouse\Federation\EntityResolverProvider;
|
||||
|
||||
/**
|
||||
* @see \MLL\GraphQLScalars\MixedScalar
|
||||
*/
|
||||
class Any extends ScalarType
|
||||
{
|
||||
const MESSAGE = 'Expected an input with a field `__typename` and matching fields, got: ';
|
||||
|
||||
public $name = '_Any';
|
||||
|
||||
public $description = /** @lang Markdown */ <<<'DESCRIPTION'
|
||||
Representation of entities from external services for the root `_entities` field.
|
||||
DESCRIPTION;
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{__typename: string}
|
||||
*/
|
||||
public function parseValue($value): array
|
||||
{
|
||||
// We do as much validation as possible here, before entering resolvers
|
||||
|
||||
if (! is_array($value)) {
|
||||
throw new Error(self::MESSAGE.\Safe\json_encode($value));
|
||||
}
|
||||
|
||||
$typename = $value['__typename'] ?? null;
|
||||
if (! is_string($typename)) {
|
||||
throw new Error(self::MESSAGE.\Safe\json_encode($value));
|
||||
}
|
||||
|
||||
/** @var \Nuwave\Lighthouse\Federation\EntityResolverProvider $entityResolverProvider */
|
||||
$entityResolverProvider = app(EntityResolverProvider::class);
|
||||
|
||||
// Representations must contain at least the fields defined in the fieldset of a @key directive on the base type.
|
||||
$definition = $entityResolverProvider->typeDefinition($typename);
|
||||
$keyFieldsSelections = $entityResolverProvider->keyFieldsSelections($definition);
|
||||
$entityResolverProvider->firstSatisfiedKeyFields($keyFieldsSelections, $value);
|
||||
|
||||
// Ensure we actually have a resolver for the type available
|
||||
$entityResolverProvider->resolver($typename);
|
||||
|
||||
// @phpstan-ignore-next-line type inference is too weak
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{__typename: string}
|
||||
*/
|
||||
public function parseLiteral($valueNode, ?array $variables = null): array
|
||||
{
|
||||
return $this->parseValue(
|
||||
AST::valueFromASTUntyped($valueNode)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Federation\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
/**
|
||||
* Only necessary for schema validation, not used at runtime.
|
||||
*/
|
||||
class FieldSet extends ScalarType
|
||||
{
|
||||
public function serialize($value)
|
||||
{
|
||||
}
|
||||
|
||||
public function parseValue($value)
|
||||
{
|
||||
}
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user