Skip to content

Commit

Permalink
Add support for @interfaceObject directive (#96)
Browse files Browse the repository at this point in the history
* Add support for `@interfaceObject` directive
  • Loading branch information
kzlsakal authored Apr 23, 2024
1 parent bb4cfda commit cc78b65
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
inputs: ["{mix,.formatter}.exs", "{config,lib,test,federation_compatibility}/**/*.{ex,exs}"],
line_length: 120,
import_deps: [:absinthe]
]
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,25 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
absinthe_federation-*.tar

# Federation test artifacts
supergraph-compose.yaml
supergraph-config.yaml
supergraph.graphql
results.md

# Local temporary Dockerfiles
federation_compatibility_dockerfile.local

# Local text editor files
.vscode

# Local version config
.mise.toml
.tool-versions*

# Local OS-specific files
.DS_Store

# PLT files
/priv/plts/*.plt
/priv/plts/*.plt.hash
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,19 @@ defmodule MyApp.MySchema do

+ extend schema do
+ directive :link,
+ url: "https://specs.apollo.dev/federation/v2.0",
+ url: "https://specs.apollo.dev/federation/v2.3",
+ import: [
+ "@key",
+ "@shareable",
+ "@provides",
+ "@requires",
+ "@external",
+ "@tag",
+ "@extends",
+ "@override",
+ "@inaccessible"
+ "@inaccessible",
+ "@composeDirective",
+ "@interfaceObject"
+ ]
+ end

Expand Down
47 changes: 35 additions & 12 deletions federation_compatibility/lib/products_web/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule ProductsWeb.Schema do
directive :link, url: "https://divvypay.com/test/v2.4", import: ["@custom"]

directive :link,
url: "https://specs.apollo.dev/federation/v2.1",
url: "https://specs.apollo.dev/federation/v2.3",
import: [
"@extends",
"@external",
Expand All @@ -51,7 +51,8 @@ defmodule ProductsWeb.Schema do
"@requires",
"@shareable",
"@tag",
"@composeDirective"
"@composeDirective",
"@interfaceObject"
]
end

Expand Down Expand Up @@ -256,6 +257,23 @@ defmodule ProductsWeb.Schema do
end
end

@desc """
type Inventory @interfaceObject @key(fields: "id") {
id: ID! @external
deprecatedProducts: [DeprecatedProduct!]!
}
"""
object :inventory do
key_fields("id")
interface_object()

field :id, non_null(:id), do: external()

field :deprecated_products, non_null(list_of(non_null(:deprecated_product))) do
resolve &resolve_deprecated_products_for_inventory/3
end
end

defp resolve_product(_parent, %{id: id}, _ctx) do
{:ok, Enum.find(products(), &(&1.id == id))}
end
Expand Down Expand Up @@ -307,16 +325,8 @@ defmodule ProductsWeb.Schema do
end
end

defp resolve_deprecated_product_reference(
%{sku: "apollo-federation-v1", package: "@apollo/federation-v1"},
_ctx
) do
{:ok,
%DeprecatedProduct{
sku: "apollo-federation-v1",
package: "@apollo/federation-v1",
reason: "Migrate to Federation V2"
}}
defp resolve_deprecated_product_reference(%{sku: sku}, _ctx) do
{:ok, Enum.find(deprecated_products(), &(&1.sku == sku))}
end

defp resolve_deprecated_product_reference(_args, _ctx) do
Expand All @@ -339,6 +349,10 @@ defmodule ProductsWeb.Schema do
{:ok, nil}
end

defp resolve_deprecated_products_for_inventory(%{__typename: "Inventory"} = _parent, _args, _ctx) do
{:ok, deprecated_products()}
end

defp resolve_deprecated_product_created_by(_deprecated_product, _args, _ctx) do
{:ok, List.first(users())}
end
Expand All @@ -363,6 +377,15 @@ defmodule ProductsWeb.Schema do
}
]

defp deprecated_products(),
do: [
%DeprecatedProduct{
sku: "apollo-federation-v1",
package: "@apollo/federation-v1",
reason: "Migrate to Federation V2"
}
]

defp product_research(),
do: [
%ProductResearch{
Expand Down
8 changes: 8 additions & 0 deletions federation_compatibility_dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
FROM elixir:1.14.2-alpine AS build
ADD https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem /usr/local/share/ca-certificates/aws-rds.crt
ADD https://mobile.zscaler.net/downloads/zscaler2048_sha256.crt /usr/local/share/ca-certificates/zscaler.crt
RUN cat /usr/local/share/ca-certificates/*.crt >> /etc/ssl/certs/ca-certificates.crt

WORKDIR /
COPY . .
Expand Down Expand Up @@ -28,6 +31,11 @@ RUN mix do compile, release

# prepare release image
FROM alpine:3.16 AS app
ADD https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem /usr/local/share/ca-certificates/aws-rds.crt
ADD https://mobile.zscaler.net/downloads/zscaler2048_sha256.crt /usr/local/share/ca-certificates/zscaler.crt
RUN cat /usr/local/share/ca-certificates/*.crt >> /etc/ssl/certs/ca-certificates.crt

RUN apk update
RUN apk add --no-cache openssl libgcc libstdc++ ncurses-libs

WORKDIR /app
Expand Down
49 changes: 49 additions & 0 deletions lib/absinthe/federation/notation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,52 @@ defmodule Absinthe.Federation.Notation do
end
end

@doc """
Adds the `@interfaceObject` directive to the field which indicates that the
object definition serves as an abstraction of another subgraph's entity
interface. This abstraction enables a subgraph to automatically contribute
fields to all entities that implement a particular entity interface.
During composition, the fields of every `@interfaceObject` are added both to
their corresponding interface definition and to all entity types that
implement that interface.
More information can be found on:
https://www.apollographql.com/docs/federation/federated-types/interfaces
## Example
object :media do
key_fields("id")
interface_object()
field :id, non_null(:id), do: external()
field :reviews, non_null(list_of(non_null(:review)))
end
object :review do
field :score, non_null(:integer)
end
## SDL Output
type Media @interfaceObject @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
type Review {
score: Int!
}
"""
defmacro interface_object() do
quote do
meta :interface_object, true
end
end

@doc """
The `@tag` directive indicates whether to include or exclude the field/type from your contract schema.
Expand Down Expand Up @@ -309,6 +355,9 @@ defmodule Absinthe.Federation.Notation do
The `@link` directive links definitions from an external specification to this schema.
Every Federation 2 subgraph uses the `@link` directive to import the other federation-specific directives.
**NOTE:** If you're using Absinthe v1.7.1 or later, instead of using this macro, it's preferred to use the
`extend schema` method you can find in the [README](README.md#federation-v2).
## Example
link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@tag", "@shareable"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ defmodule Absinthe.Federation.Schema.Phase.AddFederatedDirectives do
|> maybe_add_shareable_directive(meta)
|> maybe_add_override_directive(meta)
|> maybe_add_inaccessible_directive(meta)
|> maybe_add_interface_object_directive(meta)
|> maybe_add_tag_directive(meta)
end

Expand Down Expand Up @@ -107,6 +108,14 @@ defmodule Absinthe.Federation.Schema.Phase.AddFederatedDirectives do

defp maybe_add_inaccessible_directive(node, _meta), do: node

defp maybe_add_interface_object_directive(node, %{interface_object: true, absinthe_adapter: adapter}) do
directive = Directive.build("interface_object", adapter)

add_directive(node, directive)
end

defp maybe_add_interface_object_directive(node, _meta), do: node

defp maybe_add_tag_directive(node, %{tag: name, absinthe_adapter: adapter}) do
directive = Directive.build("tag", adapter, name: name)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ defmodule Absinthe.Federation.Schema.Prototype.FederatedDirectives do
]
end

@desc """
Indicates that an object definition serves as an abstraction of another subgraph's entity interface.
This abstraction enables a subgraph to automatically contribute fields to all entities that implement
a particular entity interface.
"""
directive :interface_object do
on [:object]
end

@desc """
The `@tag` directive indicates whether to include or exclude the field/type from your contract schema.
"""
Expand Down
32 changes: 32 additions & 0 deletions test/absinthe/federation/notation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,37 @@ defmodule Absinthe.Federation.NotationTest do
assert sdl =~ ~s{directive @custom on SCHEMA}
assert sdl =~ ~s{directive @other on OBJECT}
end

test "schema with an interfaceObject is valid" do
defmodule InterfaceObjectSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

extend schema do
directive :link, url: "https://specs.apollo.dev/federation/v2.3", import: ["@interfaceObject", "@key"]
end

query do
field :hello, :media
end

object :media do
key_fields("id")
interface_object()

field :id, non_null(:id), do: external()
field :reviews, non_null(list_of(non_null(:review)))
end

object :review do
field :score, non_null(:integer)
end
end

sdl = Absinthe.Schema.to_sdl(InterfaceObjectSchema)

assert sdl =~ ~s{import: ["@interfaceObject", "@key"])}
assert sdl =~ ~s{type Media @interfaceObject @key(fields: "id")}
end
end
end

0 comments on commit cc78b65

Please sign in to comment.