From 2ae595125d05034ebc716969e6d1ece5ed9e1e18 Mon Sep 17 00:00:00 2001 From: Benjamin Kimball Date: Wed, 31 Jul 2024 14:00:00 -0600 Subject: [PATCH] Update README to include examples from Apollo Federation docs (#100) * Update README to include examples from Apollo Federation docs Co-authored-by: Doruk Gurleyen --------- Co-authored-by: Doruk Gurleyen --- README.md | 175 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index cf4d20a..f16ed6e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # 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](http://img.shields.io/hexpm/v/absinthe_federation.svg)](https://hex.pm/packages/absinthe_federation) +[![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/federation-spec/) support for [Absinthe](https://github.com/absinthe-graphql/absinthe) +[Apollo Federation](https://www.apollographql.com/docs/federation) support for [Absinthe](https://hexdocs.pm/absinthe/overview.html). ## Installation -Install from [Hex.pm](https://hex.pm/packages/absinthe_federation): +Install from [Hex](https://hex.pm/packages/absinthe_federation): ```elixir def deps do @@ -19,7 +19,7 @@ def deps do end ``` -Install from github: +Install a specific branch from [GitHub](https://github.com/DivvyPayHQ/absinthe_federation): ```elixir def deps do @@ -29,10 +29,10 @@ def deps do end ``` -Add the following line to your absinthe schema +Use `Absinthe.Federation.Schema` module in your root schema: ```elixir -defmodule MyApp.MySchema do +defmodule Example.Schema do use Absinthe.Schema + use Absinthe.Federation.Schema @@ -42,56 +42,160 @@ defmodule MyApp.MySchema do end ``` -## Usage +Validate everything is wired up correctly: -### Macro based schemas (recommended) +```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) -> Note: Implementing the reference resolver with function capture does not work at the moment. Hence, the below example uses an anonymous function. +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 MyApp.MySchema do +defmodule Products.Schema do use Absinthe.Schema -+ use Absinthe.Federation.Schema + use Absinthe.Federation.Schema - query do -+ extends() + extend schema do + directive(:link, + url: "https://specs.apollo.dev/federation/v2.3", + import: ["@key", ...] + ) + end - field :review, :review do - arg(:id, non_null(:id)) - resolve(&ReviewResolver.get_review_by_id/3) + 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 -+ key_fields("upc") -+ extends() + directive :key, fields: "id" - field :upc, non_null(:string) do -+ external() + # 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(:reviews, list_of(:review)) do - resolve(&ReviewResolver.get_reviews_for_product/3) - end + field :id, non_null(:string) + field :in_stock, non_null(:boolean) + end -+ field(:_resolve_reference, :product) do -+ resolve(fn parent, args, context -> - ProductResolver.get_product_by_upc(parent, args, context) + 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 + 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 +If you are already using a schema prototype. ```elixir -defmodule MyApp.MySchema do +defmodule Example.Schema do use Absinthe.Schema -+ use Absinthe.Federation.Schema, prototype_schema: MyApp.MySchemaPrototype ++ use Absinthe.Federation.Schema, prototype_schema: Example.SchemaPrototype query do ... @@ -100,7 +204,7 @@ end ``` ```elixir -defmodule MyApp.MySchemaPrototype do +defmodule Example.SchemaPrototype do use Absinthe.Schema.Prototype + use Absinthe.Federation.Schema.Prototype.FederatedDirectives @@ -113,7 +217,7 @@ end ### SDL based schemas (experimental) ```elixir -defmodule MyApp.MySchema do +defmodule Example.Schema do use Absinthe.Schema + use Absinthe.Federation.Schema @@ -131,6 +235,7 @@ defmodule MyApp.MySchema do def hydrate(_, _) do ... end +end ``` ### Resolving structs in \_entities queries @@ -156,7 +261,7 @@ end You can import Apollo Federation v2 directives by extending your top-level schema with the `@link` directive. ```elixir -defmodule MyApp.MySchema do +defmodule Example.Schema do use Absinthe.Schema use Absinthe.Federation.Schema @@ -189,13 +294,13 @@ end `@link` directive supports namespacing and directive renaming (only on **Absinthe >= 1.7.2**) according to the specs. ```elixir -defmodule MyApp.MySchema do +defmodule Example.Schema do use Absinthe.Schema use Absinthe.Federation.Schema + extend schema do + directive :link, -+ url: "https://specs.apollo.dev/federation/v2.0", ++ url: "https://specs.apollo.dev/federation/v2.3", + import: [%{"name" => "@key", "as" => "@primaryKey"}], # directive renaming + as: "federation" # namespacing + end