Skip to content

Adds Apollo Federation Spec conformance to the absinthe GraphQL library

License

Notifications You must be signed in to change notification settings

DivvyPayHQ/absinthe_federation

Repository files navigation

Absinthe.Federation

Build Status Hex pm Hex Docs License

Apollo Federation support for Absinthe.

Installation

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.

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.

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

Macro based schema with existing prototype

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

SDL based schemas (experimental)

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:

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.

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.

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.

Contributing

Refer to the Contributing Guide.

License

See LICENSE