Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/divvypayhq/absinthe_federation

Adds Apollo Federation Spec conformance to the absinthe GraphQL library
https://github.com/divvypayhq/absinthe_federation

absinthe-graphql apollo-federation elixir graphql

Last synced: 15 days ago
JSON representation

Adds Apollo Federation Spec conformance to the absinthe GraphQL library

Awesome Lists containing this project

README

        

# Absinthe.Federation

[![Build Status](https://github.com/DivvyPayHQ/absinthe_federation/workflows/CI/badge.svg)](https://github.com/DivvyPayHQ/absinthe_federation/actions?query=workflow%3ACI)
[![Hex pm](https://img.shields.io/hexpm/v/absinthe_federation.svg)](https://hex.pm/packages/absinthe_federation)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/absinthe_federation/)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

[Apollo Federation](https://www.apollographql.com/docs/federation) support for [Absinthe](https://hexdocs.pm/absinthe/overview.html).

## Installation

Install from [Hex](https://hex.pm/packages/absinthe_federation):

```elixir
def deps do
[
{:absinthe_federation, "~> 0.5"}
]
end
```

Install a specific branch from [GitHub](https://github.com/DivvyPayHQ/absinthe_federation):

```elixir
def deps do
[
{:absinthe_federation, github: "DivvyPayHQ/absinthe_federation", branch: "main"}
]
end
```

Use `Absinthe.Federation.Schema` module in your root schema:

```elixir
defmodule Example.Schema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema

query do
...
end
end
```

Validate everything is wired up correctly:

```bash
mix absinthe.federation.schema.sdl --schema Example.Schema
```

You should see the [Apollo Federation Subgraph Specification](https://www.apollographql.com/docs/federation/subgraph-spec) fields along with any fields you've defined. It can be helpful to add `*.graphql` to your `.gitignore`, at least at your projects root level, while testing your SDL output during development.

## Usage (macro based schemas)

The following sticks close to the Apollo Federation documentation to better clarify how to achieve the same outcomes with the `Absinthe.Federation` module as you'd get from their JavaScript examples.

### [Defining an entity](https://www.apollographql.com/docs/federation/entities#defining-an-entity)

```elixir
defmodule Products.Schema do
use Absinthe.Schema
use Absinthe.Federation.Schema

extend schema do
directive(:link,
url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", ...]
)
end

object :product do
directive :key, fields: "id"

# Any subgraph contributing fields MUST define a _resolve_reference field.
# Note that implementing the reference resolver with function capture does not work at the moment. Hence, the examples below use an anonymous function.
field :_resolve_reference, :product do
resolve(fn %{__typename: "Product", id: id} = entity, _info ->
{:ok, Map.merge(entity, %{name: "ACME Anvil", price: 10000})}
end)
end

field :id, non_null(:id)
field :name, non_null(:string)
field :price, :int
end

query do
...
end
end
```

Your `:_resolve_reference` must return one of the following:

```elixir
{:ok, %Product{id: id, ...}}
```

```elixir
{:ok, %{__typename: "Product", id: id, ...}}
```

```elixir
{:ok, %{"__typename" => "Product", "id" => id, ...}}
```

```elixir
{:ok, nil}
```

It is easier to just merge a subgraph's contributed fields back onto the incoming entity reference than rely on a struct to set the `__typename`.

### [Contributing entity fields](https://www.apollographql.com/docs/federation/entities#contributing-entity-fields)

Each subgraph, by default, must return different fields. See the Apollo documentation should you need to [override this behavior](https://www.apollographql.com/docs/federation/entities/resolve-another-subgraphs-fields).

```elixir
defmodule Inventory.Schema do
use Absinthe.Schema
use Absinthe.Federation.Schema

extend schema do
directive(:link,
url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", ...]
)
end

object :product do
directive :key, fields: "id"

# In this case, only the `Inventory.Schema` should resolve the `inStock` field.
field :_resolve_reference, :product do
resolve(fn %{__typename: "Product", id: id} = entity, _info ->
{:ok, Map.merge(entity, %{in_stock: true})}
end)
end

field :id, non_null(:string)
field :in_stock, non_null(:boolean)
end

query do
...
end
end
```

### [Referencing an entity without contributing fields](https://www.apollographql.com/docs/federation/entities#referencing-an-entity-without-contributing-fields)

```elixir
defmodule Reviews.Schema do
use Absinthe.Schema
use Absinthe.Federation.Schema

extend schema do
directive(:link,
url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", ...]
)
end

# Stubbed entity, marked as unresolvable in this subgraph.
object :product do
directive :key, fields: "id", resolvable: false

field :id, non_null(:string)
end

object :review do
field :id, non_null(:id)
field :score, non_null(:int)
field :description, non_null(:string)

# This subgraph only needs to resolve the key fields used to reference the entity.
field :product, non_null(:product) do
resolve(fn %{product_id: id} = _parent, _args, _info ->
{:ok, %{id: id}}
end)
end
end

query do
field :latest_reviews, non_null(list(:review)) do
resolve(&ReviewsResolver.find_many/2)
end
end
end
```

### Macro based schema with existing prototype

If you are already using a schema prototype.

```elixir
defmodule Example.Schema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema, prototype_schema: Example.SchemaPrototype

query do
...
end
end
```

```elixir
defmodule Example.SchemaPrototype do
use Absinthe.Schema.Prototype
+ use Absinthe.Federation.Schema.Prototype.FederatedDirectives

directive :my_directive do
on [:schema]
end
end
```

### SDL based schemas (experimental)

```elixir
defmodule Example.Schema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema

import_sdl """
extend type Query {
review(id: ID!): Review
}

extend type Product @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}
"""

def hydrate(_, _) do
...
end
end
```

### Resolving structs in \_entities queries

If you need to resolve your struct to a specific type in your schema you can implement the `Absinthe.Federation.Schema.EntityUnion.Resolver` protocol like this:

```elixir
defmodule MySchema do
@type t :: %__MODULE__{
id: String.t()
}

defstruct id: ""

defimpl Absinthe.Federation.Schema.EntityUnion.Resolver do
def resolve_type(_, _), do: :my_schema_object_name
end
end
```

### Federation v2

You can import Apollo Federation v2 directives by extending your top-level schema with the `@link` directive.

```elixir
defmodule Example.Schema do
use Absinthe.Schema
use Absinthe.Federation.Schema

+ extend schema do
+ directive :link,
+ url: "https://specs.apollo.dev/federation/v2.3",
+ import: [
+ "@key",
+ "@shareable",
+ "@provides",
+ "@requires",
+ "@external",
+ "@tag",
+ "@extends",
+ "@override",
+ "@inaccessible",
+ "@composeDirective",
+ "@interfaceObject"
+ ]
+ end

query do
...
end
end
```

### Namespacing and directive renaming with `@link`

`@link` directive supports namespacing and directive renaming (only on **Absinthe >= 1.7.2**) according to the specs.

```elixir
defmodule Example.Schema do
use Absinthe.Schema
use Absinthe.Federation.Schema

+ extend schema do
+ directive :link,
+ url: "https://specs.apollo.dev/federation/v2.3",
+ import: [%{"name" => "@key", "as" => "@primaryKey"}], # directive renaming
+ as: "federation" # namespacing
+ end

query do
...
end
end
```

## More Documentation

See additional documentation, including guides, in the [Absinthe.Federation hexdocs](https://hexdocs.pm/absinthe_federation).

## Contributing

Refer to the [Contributing Guide](./CONTRIBUTING.md).

## License

See [LICENSE](./LICENSE.md)