项目作者: DivvyPayHQ

项目描述 :
Adds Apollo Federation Spec conformance to the absinthe GraphQL library
高级语言: Elixir
项目地址: git://github.com/DivvyPayHQ/absinthe_federation.git
创建时间: 2021-06-17T21:15:54Z
项目社区:https://github.com/DivvyPayHQ/absinthe_federation

开源协议:MIT License

下载


Absinthe.Federation

Build Status
Hex pm
Hex Docs
License

Apollo Federation support for Absinthe.

Installation

Install from Hex:

  1. def deps do
  2. [
  3. {:absinthe_federation, "~> 0.7"}
  4. ]
  5. end

Install a specific branch from GitHub:

  1. def deps do
  2. [
  3. {:absinthe_federation, github: "DivvyPayHQ/absinthe_federation", branch: "main"}
  4. ]
  5. end

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

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. + use Absinthe.Federation.Schema
  4. query do
  5. ...
  6. end
  7. end

Validate everything is wired up correctly:

  1. 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.

Defining an entity

  1. defmodule Products.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. extend schema do
  5. directive(:link,
  6. url: "https://specs.apollo.dev/federation/v2.3",
  7. import: ["@key", ...]
  8. )
  9. end
  10. object :product do
  11. directive :key, fields: "id"
  12. # Any subgraph contributing fields MUST define a _resolve_reference field.
  13. field :_resolve_reference, :product do
  14. resolve &Products.find_by_id/2
  15. end
  16. field :id, non_null(:id)
  17. field :name, non_null(:string)
  18. field :price, :int
  19. end
  20. query do
  21. ...
  22. end
  23. end

Your :_resolve_reference must return one of the following:

  1. {:ok, %Product{id: id, ...}}
  1. {:ok, %{__typename: "Product", id: id, ...}}
  1. {:ok, %{"__typename" => "Product", "id" => id, ...}}
  1. {: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

Each subgraph, by default, must return different fields. See the Apollo documentation should you need to override this behavior.

  1. defmodule Inventory.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. extend schema do
  5. directive(:link,
  6. url: "https://specs.apollo.dev/federation/v2.3",
  7. import: ["@key", ...]
  8. )
  9. end
  10. object :product do
  11. directive :key, fields: "id"
  12. # In this case, only the `Inventory.Schema` should resolve the `inStock` field.
  13. field :_resolve_reference, :product do
  14. resolve(fn %{__typename: "Product", id: id} = entity, _info ->
  15. {:ok, Map.merge(entity, %{in_stock: true})}
  16. end)
  17. end
  18. field :id, non_null(:string)
  19. field :in_stock, non_null(:boolean)
  20. end
  21. query do
  22. ...
  23. end
  24. end

Referencing an entity without contributing fields

  1. defmodule Reviews.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. extend schema do
  5. directive(:link,
  6. url: "https://specs.apollo.dev/federation/v2.3",
  7. import: ["@key", ...]
  8. )
  9. end
  10. # Stubbed entity, marked as unresolvable in this subgraph.
  11. object :product do
  12. directive :key, fields: "id", resolvable: false
  13. field :id, non_null(:string)
  14. end
  15. object :review do
  16. field :id, non_null(:id)
  17. field :score, non_null(:int)
  18. field :description, non_null(:string)
  19. # This subgraph only needs to resolve the key fields used to reference the entity.
  20. field :product, non_null(:product) do
  21. resolve(fn %{product_id: id} = _parent, _args, _info ->
  22. {:ok, %{id: id}}
  23. end)
  24. end
  25. end
  26. query do
  27. field :latest_reviews, non_null(list(:review)) do
  28. resolve(&ReviewsResolver.find_many/2)
  29. end
  30. end
  31. end

Macro based schema with existing prototype

If you are already using a schema prototype.

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. + use Absinthe.Federation.Schema, prototype_schema: Example.SchemaPrototype
  4. query do
  5. ...
  6. end
  7. end
  1. defmodule Example.SchemaPrototype do
  2. use Absinthe.Schema.Prototype
  3. + use Absinthe.Federation.Schema.Prototype.FederatedDirectives
  4. directive :my_directive do
  5. on [:schema]
  6. end
  7. end

SDL based schemas (experimental)

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. + use Absinthe.Federation.Schema
  4. import_sdl """
  5. extend type Query {
  6. review(id: ID!): Review
  7. }
  8. extend type Product @key(fields: "upc") {
  9. upc: String! @external
  10. reviews: [Review]
  11. }
  12. """
  13. def hydrate(_, _) do
  14. ...
  15. end
  16. end

Using Dataloader in _resolve_reference queries

You can use Dataloader in to resolve references to specific objects, but it requires manually setting up the batch and item key, as the field has no parent. Resolution for both _resolve_reference fields are functionally equivalent.

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. import Absinthe.Resolution.Helpers, only: [on_load: 2, dataloader: 2]
  5. def context(ctx) do
  6. loader =
  7. Dataloader.new()
  8. |> Dataloader.add_source(Example.Loader, Dataloader.Ecto.new(Example.Repo))
  9. Map.put(ctx, :loader, loader)
  10. end
  11. def plugins do
  12. [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
  13. end
  14. object :item do
  15. key_fields("item_id")
  16. # Using the dataloader/2 resolution helper
  17. field :_resolve_reference, :item do
  18. resolve dataloader(Example.Loader, fn _parent, args, _res ->
  19. %{batch: {{:one, Example.Item}, %{}}, item: [item_id: args.item_id]}
  20. end)
  21. end
  22. end
  23. object :verbose_item do
  24. key_fields("item_id")
  25. # Using the on_load/2 resolution helper
  26. field :_resolve_reference, :verbose_item do
  27. resolve fn %{item_id: id}, %{context: %{loader: loader}} ->
  28. batch_key = {:one, Example.Item, %{}}
  29. item_key = [item_id: id]
  30. loader
  31. |> Dataloader.load(Example.Loader, batch_key, item_key)
  32. |> on_load(fn loader ->
  33. result = Dataloader.get(loader, Example.Loader, batch_key, item_key)
  34. {:ok, result}
  35. end)
  36. end
  37. end
  38. 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:

  1. defmodule MySchema do
  2. @type t :: %__MODULE__{
  3. id: String.t()
  4. }
  5. defstruct id: ""
  6. defimpl Absinthe.Federation.Schema.EntityUnion.Resolver do
  7. def resolve_type(_, _), do: :my_schema_object_name
  8. end
  9. end

Federation v2

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

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. + extend schema do
  5. + directive :link,
  6. + url: "https://specs.apollo.dev/federation/v2.7",
  7. + import: [
  8. "@authenticated",
  9. "@extends",
  10. "@external",
  11. "@inaccessible",
  12. "@key",
  13. "@override",
  14. "@policy",
  15. "@provides",
  16. "@requires",
  17. "@requiresScopes",
  18. "@shareable",
  19. "@tag",
  20. "@composeDirective",
  21. "@interfaceObject"
  22. + ]
  23. + end
  24. query do
  25. ...
  26. end
  27. end

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

  1. defmodule Example.Schema do
  2. use Absinthe.Schema
  3. use Absinthe.Federation.Schema
  4. + extend schema do
  5. + directive :link,
  6. + url: "https://specs.apollo.dev/federation/v2.3",
  7. + import: [%{"name" => "@key", "as" => "@primaryKey"}], # directive renaming
  8. + as: "federation" # namespacing
  9. + end
  10. query do
  11. ...
  12. end
  13. end

More Documentation

See additional documentation, including guides, in the Absinthe.Federation hexdocs.

Contributing

Refer to the Contributing Guide.

License

See LICENSE