Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for @interfaceObject directive #96

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading