README.md

# 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)