Skip to content

Commit

Permalink
Improve federation docs
Browse files Browse the repository at this point in the history
  • Loading branch information
rprybudko authored Aug 17, 2023
1 parent 989d0ed commit 21ae33c
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 198 deletions.
98 changes: 5 additions & 93 deletions docs/6/federation/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,10 @@

A core component of a federation capable GraphQL service is the `_entities` field.
For a given `__typename` in the given `$representations`, Lighthouse will look for
a resolver to return the full `_Entity`.
a [reference resolver](reference-resolvers.md) to return the full `_Entity`.

## Class Based Resolvers
## Extends

Lighthouse will look for a class which name is equivalent to `__typename` in the
namespace configured in `lighthouse.federation.entities_resolver_namespace`.

```graphql
{
_entities(representations: [{ __typename: "Foo", id: 1 }]) {
... on Foo {
id
}
}
}
```

### Single Entity Resolvers

After validating the type `Foo` exists, Lighthouse will look for a resolver class in `App\GraphQL\Entities\Foo`.
The resolver class is expected to contain a method `__invoke()` which takes a single argument:
the array form of the representation.

```php
namespace App\GraphQL\Entities;

final class Foo
{
/**
* @param array{__typename: string, id: int} $representation
*/
public function __invoke(array $representation)
{
// TODO return a value that matches type Foo
}
}
```

### Batched Entity Resolves

When the client requests a large number of entities with the same type, it can be more efficient to resolve
them all at once. When your entity resolver class implements `Nuwave\Lighthouse\Federation\BatchedEntityResolver`,
Lighthouse will call it a single time with an array of all representations of its type. The resolver can then do
some kind of batch query to resolve them and return them all at once.

```php
namespace App\GraphQL\Entities;

use Nuwave\Lighthouse\Federation\BatchedEntityResolver;

final class Foo implements BatchedEntityResolver
{
/**
* @param array<string, array{__typename: string, id: int}> $representations
*/
public function __invoke(array $representations): iterable
{
// TODO return multiple values that match type Foo
}
}
```

The returned iterable _must_ have the same keys as the given `array $representations` to enable Lighthouse
to return the results in the correct order.

## Eloquent Model Resolvers

When no resolver class can be found, Lighthouse will attempt to find the model that
matches the type `__typename`, using the namespaces configured in `lighthouse.namespaces.models`.

```graphql
{
_entities(representations: [{ __typename: "Foo", bar: "asdf", baz: 42 }]) {
... on Foo {
id
}
}
}
```

The additional fields in the representation constrain the query builder, which is then
called and expected to return a single result.

```php
$results = App\Models\Foo::query()
->where('bar', 'asdf')
->where('baz', 42)
->get();

if ($results->count() > 1) {
throw new Error('The query returned more than one result.');
}

return $results->first();
```
You have to use `@extends` in place of `extend type` to annotate type references.
This is because Lighthouse merges type extensions before the final schema is produced,
thus they would not be preserved to appear in the federation schema SDL.
103 changes: 103 additions & 0 deletions docs/6/federation/entity-representations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Entity representation

To reference an entity that originates in another subgraph, first subgraph needs to define a stub of that entity to make its own schema valid.
The stub includes just enough information for the subgraph to know how to uniquely identify a particular entity in another subgraph.
Read more about the entity representation in the [Apollo Federation docs](https://www.apollographql.com/docs/federation/v1/entities/#entity-representations).

A representation always consists of:
- A `__typename` field;
- Values for the entity's primary key fields.

## Eloquent Model representation

If there is an Eloquent relationship between entities from different subgraphs, it's not mandatory to define an entity representation.
Lighthouse will automatically determine the necessary information based on the relationship directive.

```graphql
type Post {
id: ID!
title: String!
comments: [Comment!]! @hasMany
}

type Comment @extends @key(fields: "id") {
id: ID! @external
}
```

## Non-Eloquent representation

If entities don't have an Eloquent relationship within the subgraph, it's necessary to specify a separate resolver that will return the required information.
The resolver should return data containing information about the `__typename` field, which corresponds to the entity's name and the primary key that can identify the entity.
The `__typename` can either be provided as an explicit field or implicitly by returning an object with a matching class name.

### Example 1

In this example, subgraph for order service has an entity called `Order`, which in turn has an entity called `Receipt`
defined in a separate subgraph for payment service. The relationship between `Order` and `Receipt` is one-to-one.

```graphql
type Order {
id: ID!
receipt: Receipt!
}

type Receipt @extends @key(fields: "uuid") {
uuid: ID! @external
}
```

The resolver for receipt in order service returns an array consisting of:
- `__typename` - the entity name from the payment service;
- `uuid` - the primary key of the receipt.

```php
namespace App\GraphQL\Types\Order;

final class Receipt
{
public function __invoke($order): array
{
return [
'__typename' => 'Receipt',
'uuid' => $order->receipt_id,
];
}
}
```

### Example 2

In this example, subgraph for order service has an entity called `Order`, which in turn has an entity called `Product`
defined in a separate subgraph for product service. The relationship between `Order` and `Product` is one-to-many.

```graphql
type Order {
sum: Int!
products: [Product!]!
}

type Product @extends @key(fields: "uuid") {
uuid: ID! @external
}
```

The resolver for product in order service returns an array of arrays. Each sub-array consists of:
- `__typename` - the entity name from the product service;
- `uuid` - the primary key of a specific product.

```php
namespace App\GraphQL\Types\Order;

final class Products
{
public function __invoke($order): iterable
{
return ProductService::retrieveProductsForOrder($order)
->map(fn (Product $product): array => [
'__typename' => 'Product',
'uuid' => $product->id,
]);
}
}
```
6 changes: 0 additions & 6 deletions docs/6/federation/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,3 @@ Add the service provider to your `config/app.php`:
\Nuwave\Lighthouse\Federation\FederationServiceProvider::class,
],
```

## Extends

You have to use `@extends` in place of `extend type` to annotate type references.
This is because Lighthouse merges type extensions before the final schema is produced,
thus they would not be preserved to appear in the federation schema SDL.
148 changes: 148 additions & 0 deletions docs/6/federation/reference-resolvers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Reference resolvers

To enable the current subgraph to provide entities for other subgraphs, you need to implement reference resolvers. These
reference resolvers act as helpers to enable cross-subgraph communication and provide data from one subgraph to another
when needed. Read more about the reference resolvers in
the [Apollo Federation docs](https://www.apollographql.com/docs/federation/v1/entities#reference-resolvers).

Lighthouse will look for a class which name is equivalent to `__typename` in the
namespace configured in `lighthouse.federation.entities_resolver_namespace`.

When you need to retrieve information from subgraphs, the gateway automatically generates a request to the corresponding
endpoint of the subgraph. More details about this can be found in
section [Query._entities of the Apollo Federation docs](https://www.apollographql.com/docs/federation/building-supergraphs/subgraphs-overview#query_entities).

An example of such a request is shown below:

```graphql
{
_entities(representations: [{ __typename: "Foo", id: 1 }]) {
... on Foo {
id
}
}
}
```

## Single Entity Resolvers

After validating that type `Foo` exists, Lighthouse will look for a resolver class in
the namespace configured in `lighthouse.federation.entities_resolver_namespace`. The resolver class is expected to
contain a method `__invoke()` which takes a single argument: the array form of the representation.

```php
namespace App\GraphQL\ReferenceResolvers;

final class Foo
{
/** @param array{__typename: string, id: int} $representation */
public function __invoke(array $representation)
{
// TODO return a value that matches type Foo
}
}
```

The method should return an object that has the same name as the entity, or additionally return the field `__typename`.

```php
namespace App\GraphQL\ReferenceResolvers;

use App\Repositories\FooRepository;
use Illuminate\Support\Arr;

final class Foo
{
public function __invoke($representation): array
{
$id = Arr::get($representation, 'id');
$foo = FooRepository::byID($id)->toArray();

return Arr::add($foo, '__typename', 'Foo');
}
}
```

## Batched Entity Resolvers

When the client requests a large number of entities with the same type, it can be more efficient to resolve
them all at once. When your entity resolver class implements `Nuwave\Lighthouse\Federation\BatchedEntityResolver`,
Lighthouse will call it a single time with an array of all representations of its type. The resolver can then do
some kind of batch query to resolve them and return them all at once.

```php
namespace App\GraphQL\ReferenceResolvers;

use Nuwave\Lighthouse\Federation\BatchedEntityResolver;

final class Foo implements BatchedEntityResolver
{
/**
* @param array<string, array{__typename: string, id: int}> $representations
*/
public function __invoke(array $representations): iterable
{
// TODO return multiple values that match type Foo
}
}
```

The returned iterable _must_ have the same keys as the given `array $representations` to enable Lighthouse
to return the results in the correct order.

```php
namespace App\GraphQL\ReferenceResolvers;

use App\Repositories\ProductRepository;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Federation\BatchedEntityResolver;

final class Product implements BatchedEntityResolver
{
public function __invoke(array $representations): iterable
{
$products = ProductRepository::byIDs(Arr::pluck($representations, 'id'));

$result = [];
foreach ($representations as $key => $representation) {
$result[$key] = $products->firstWhere('id', $representation['id']);
}

return $result;
}
}
```

## Eloquent Model Resolvers

When no resolver class can be found, Lighthouse will attempt to find the model that
matches the type `__typename`, using the namespaces configured in `lighthouse.namespaces.models`.

```graphql
{
_entities(representations: [{ __typename: "Foo", bar: "asdf", baz: 42 }]) {
... on Foo {
id
}
}
}
```

The additional fields in the representation constrain the query builder, which is then
called and expected to return a single result. In simplified terms, Lighthouse will do this:

```php
$results = App\Models\Foo::query()
->where('bar', 'asdf')
->where('baz', 42)
->get();

if ($results->count() > 1) {
throw new GraphQL\Error\Error('The query returned more than one result.');
}

return $results->first();
```

The default model resolver makes one database query for each entity. Therefore, for a large number of entities, it is
worth considering [Batched Entity Resolvers](reference-resolvers.md#batched-entity-resolvers) to avoid this issue.
2 changes: 2 additions & 0 deletions docs/6/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ module.exports = [
children: [
["federation/getting-started", "Getting Started"],
"federation/entities",
"federation/entity-representations",
"federation/reference-resolvers",
],
},
{
Expand Down
Loading

0 comments on commit 21ae33c

Please sign in to comment.