Apollo Federation support for Absinthe.
Install from Hex:
def deps do
[
{:absinthe_federation, "~> 0.5"}
]
end
Install a specific branch from GitHub:
def deps do
[
{:absinthe_federation, github: "DivvyPayHQ/absinthe_federation", branch: "main"}
]
end
Use Absinthe.Federation.Schema
module in your root schema:
defmodule Example.Schema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema
query do
...
end
end
Validate everything is wired up correctly:
mix absinthe.federation.schema.sdl --schema Example.Schema
You should see the Apollo Federation Subgraph Specification 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.
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.
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:
{:ok, %Product{id: id, ...}}
{:ok, %{__typename: "Product", id: id, ...}}
{:ok, %{"__typename" => "Product", "id" => id, ...}}
{: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
.
Each subgraph, by default, must return different fields. See the Apollo documentation should you need to override this behavior.
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
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
If you are already using a schema prototype.
defmodule Example.Schema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema, prototype_schema: Example.SchemaPrototype
query do
...
end
end
defmodule Example.SchemaPrototype do
use Absinthe.Schema.Prototype
+ use Absinthe.Federation.Schema.Prototype.FederatedDirectives
directive :my_directive do
on [:schema]
end
end
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
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:
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
You can import Apollo Federation v2 directives by extending your top-level schema with the @link
directive.
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
@link
directive supports namespacing and directive renaming (only on Absinthe >= 1.7.2) according to the specs.
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
See additional documentation, including guides, in the Absinthe.Federation hexdocs.
Refer to the Contributing Guide.
See LICENSE