Skip to content

Commit

Permalink
Don't add _Entity/_entities when no @key types found
Browse files Browse the repository at this point in the history
  • Loading branch information
kdawgwilk committed Jul 16, 2021
1 parent fecd3eb commit dc70980
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 99 deletions.
62 changes: 35 additions & 27 deletions lib/absinthe/federation/schema/entities_field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
alias Absinthe.Blueprint.TypeReference.NonNull
alias Absinthe.Schema.Notation

alias Absinthe.Federation.Schema.Utils

# TODO: Fix __reference__ typespec upstream in absinthe
@type input_value_definition :: %InputValueDefinition{
name: String.t(),
Expand Down Expand Up @@ -51,34 +53,40 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
__private__: []
}

@spec build() :: field_definition()
def build() do
%FieldDefinition{
__reference__: Notation.build_reference(__ENV__),
description: """
Returns a non-nullable list of _Entity types
and have a single argument with an argument name of representations
and type [_Any!]! (non-nullable list of non-nullable _Any scalars).
The _entities field on the query root must allow a list of _Any scalars
which are "representations" of entities from external services.
These representations should be validated with the following rules:
- Any representation without a __typename: String field is invalid.
- Representations must contain at least the fields defined in the fieldset of a @key directive on the base type.
""",
identifier: :_entities,
module: __MODULE__,
name: "_entities",
type: %NonNull{
of_type: %ListType{
of_type: %Name{
name: "_Entity"
}
@spec build(Blueprint.t()) :: field_definition() | nil
def build(blueprint) do
case Utils.key_field_types(blueprint) do
[] ->
nil

_found_types ->
%FieldDefinition{
__reference__: Notation.build_reference(__ENV__),
description: """
Returns a non-nullable list of _Entity types
and have a single argument with an argument name of representations
and type [_Any!]! (non-nullable list of non-nullable _Any scalars).
The _entities field on the query root must allow a list of _Any scalars
which are "representations" of entities from external services.
These representations should be validated with the following rules:
- Any representation without a __typename: String field is invalid.
- Representations must contain at least the fields defined in the fieldset of a @key directive on the base type.
""",
identifier: :_entities,
module: __MODULE__,
name: "_entities",
type: %NonNull{
of_type: %ListType{
of_type: %Name{
name: "_Entity"
}
}
},
middleware: [{Absinthe.Resolution, &__MODULE__.resolver/3}],
arguments: build_arguments()
}
},
middleware: [{Absinthe.Resolution, &__MODULE__.resolver/3}],
arguments: build_arguments()
}
end
end

def resolver(parent, %{representations: representations}, resolution) do
Expand Down
59 changes: 18 additions & 41 deletions lib/absinthe/federation/schema/entity_union.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ defmodule Absinthe.Federation.Schema.EntityUnion do

alias Absinthe.Blueprint
alias Absinthe.Blueprint.Schema.UnionTypeDefinition
alias Absinthe.Blueprint.TypeReference.Name
alias Absinthe.Schema.Notation
alias Absinthe.Type

alias Absinthe.Federation.Schema.Utils

@spec build(Blueprint.t()) :: UnionTypeDefinition.t() | nil
def build(blueprint) do
%UnionTypeDefinition{
__reference__: Notation.build_reference(__ENV__),
description: "a union of all types that use the @key directive",
identifier: :_entity,
module: __MODULE__,
name: "_Entity",
types: types(blueprint),
resolve_type: &Absinthe.Federation.Schema.EntityUnion.resolve_type/2
}
case Utils.key_field_types(blueprint) do
[] ->
nil

found_types ->
%UnionTypeDefinition{
__reference__: Notation.build_reference(__ENV__),
description: "a union of all types that use the @key directive",
identifier: :_entity,
module: __MODULE__,
name: "_Entity",
types: found_types,
resolve_type: &Absinthe.Federation.Schema.EntityUnion.resolve_type/2
}
end
end

# TODO: This is a very naive approach to resolve the union type and should be replaced by something better
Expand All @@ -40,34 +47,4 @@ defmodule Absinthe.Federation.Schema.EntityUnion do
|> Macro.underscore()
|> String.to_existing_atom()
end

defp types(node) do
{_node, types} = Blueprint.postwalk(node, [], &collect_types/2)

types
end

defp collect_types(
%{name: name, __private__: _private} = node,
types
) do
if has_key_directive?(node) do
{node, [%Name{name: name} | types]}
else
{node, types}
end
end

defp collect_types(node, acc), do: {node, acc}

defp has_key_directive?(node) do
meta = Type.meta(node)
has_meta_key = Map.has_key?(meta, :key_fields)
node_directives = Map.get(node, :directives, [])
has_key_directive = Enum.any?(node_directives, &is_key_directive?/1)
has_meta_key or has_key_directive
end

defp is_key_directive?(%{name: "key"} = _directive), do: true
defp is_key_directive?(_directive), do: false
end
43 changes: 32 additions & 11 deletions lib/absinthe/federation/schema/phase.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,50 @@ defmodule Absinthe.Federation.Schema.Phase do
@dialyzer {:nowarn_function, add_directive: 2}

def run(%Blueprint{} = blueprint, _) do
blueprint = Blueprint.postwalk(blueprint, &collect_types/1)
blueprint = Blueprint.postwalk(blueprint, &collect_types(&1, blueprint))
{:ok, blueprint}
end

@spec collect_types(Blueprint.node_t()) :: Blueprint.node_t()
defp collect_types(%Schema.SchemaDefinition{type_definitions: type_definitions} = node) do
entity_union = EntityUnion.build(node)
@spec collect_types(Blueprint.node_t(), Blueprint.t()) :: Blueprint.node_t()
defp collect_types(%Schema.SchemaDefinition{type_definitions: type_definitions} = node, blueprint) do
case EntityUnion.build(blueprint) do
nil ->
node

%{node | type_definitions: [entity_union | type_definitions]}
entity_union ->
%{node | type_definitions: [entity_union | type_definitions]}
end
end

defp collect_types(%Schema.ObjectTypeDefinition{identifier: :query, fields: fields} = node) do
service_field = ServiceField.build()
entities_field = EntitiesField.build()
%{node | fields: [service_field, entities_field] ++ fields}
defp collect_types(%Schema.ObjectTypeDefinition{identifier: :query, fields: fields} = node, blueprint) do
new_fields =
fields
|> add_service_field()
|> maybe_add_entities_field(blueprint)

%{node | fields: new_fields}
end

defp collect_types(%{__private__: _private} = node) do
defp collect_types(%{__private__: _private} = node, _) do
meta = Type.meta(node)
maybe_add_directives(node, meta)
end

defp collect_types(node), do: node
defp collect_types(node, _), do: node

defp add_service_field(fields) do
[ServiceField.build() | fields]
end

defp maybe_add_entities_field(fields, node) do
case EntitiesField.build(node) do
nil ->
fields

entities_field ->
[entities_field | fields]
end
end

@spec maybe_add_directives(term(), any()) :: term()
defp maybe_add_directives(node, meta) do
Expand Down
40 changes: 40 additions & 0 deletions lib/absinthe/federation/schema/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Absinthe.Federation.Schema.Utils do
@moduledoc false

alias Absinthe.Blueprint
alias Absinthe.Blueprint.TypeReference.Name
alias Absinthe.Type

@spec key_field_types(Blueprint.t()) :: list(Blueprint.node_t())
def key_field_types(blueprint) do
{_blueprint, types} = Blueprint.postwalk(blueprint, [], &collect_key_field_types/2)

types
end

defp collect_key_field_types(
%{name: name, __private__: _private} = node,
types
) do
if has_key_directive?(node) do
{node, [%Name{name: name} | types]}
else
{node, types}
end
end

defp collect_key_field_types(node, acc), do: {node, acc}

@spec has_key_directive?(struct()) :: boolean()
def has_key_directive?(node) do
meta = Type.meta(node)
has_meta_key = Map.has_key?(meta, :key_fields)
node_directives = Map.get(node, :directives, [])
has_key_directive = Enum.any?(node_directives, &is_key_directive?/1)
has_meta_key or has_key_directive
end

@spec is_key_directive?(Blueprint.Directive.t()) :: boolean()
def is_key_directive?(%{name: "key"} = _directive), do: true
def is_key_directive?(_directive), do: false
end
68 changes: 54 additions & 14 deletions test/absinthe/federation/schema/entities_field_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,40 @@ defmodule Absinthe.Federation.Schema.EntitiesFieldTest do

alias Absinthe.Federation.Schema.EntitiesField

defmodule EntitiesSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

query do
end

object :foo do
key_fields("id")
field :id, :id
end
end

setup do
{:ok, blueprint: EntitiesSchema.__absinthe_blueprint__()}
end

describe "build" do
test "builds field definition" do
assert %FieldDefinition{} = EntitiesField.build()
test "builds field definition", %{blueprint: blueprint} do
assert %FieldDefinition{} = EntitiesField.build(blueprint)
end

test "builds field definition with name" do
field_definition = EntitiesField.build()
test "builds field definition with name", %{blueprint: blueprint} do
field_definition = EntitiesField.build(blueprint)
assert field_definition.name == "_entities"
end

test "builds field definition with identifier" do
field_definition = EntitiesField.build()
test "builds field definition with identifier", %{blueprint: blueprint} do
field_definition = EntitiesField.build(blueprint)
assert field_definition.identifier == :_entities
end

test "builds field definition with type" do
field_definition = EntitiesField.build()
test "builds field definition with type", %{blueprint: blueprint} do
field_definition = EntitiesField.build(blueprint)

assert %NonNull{
of_type: %List{
Expand All @@ -35,8 +52,8 @@ defmodule Absinthe.Federation.Schema.EntitiesFieldTest do
} = field_definition.type
end

test "builds field definition with middleware" do
field_definition = EntitiesField.build()
test "builds field definition with middleware", %{blueprint: blueprint} do
field_definition = EntitiesField.build(blueprint)
assert Enum.count(field_definition.middleware) == 1
end
end
Expand Down Expand Up @@ -73,18 +90,41 @@ defmodule Absinthe.Federation.Schema.EntitiesFieldTest do
end

describe "sdl" do
defmodule SDLSchema do
defmodule SDLWithKeyFieldsSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

query do
field :test, :string
field :user, :user
end

object :user do
key_fields("id")
field :id, non_null(:id)
end
end

test "renders correctly in sdl" do
sdl = Absinthe.Schema.to_sdl(SDLSchema)
test "renders correctly in sdl with @key" do
sdl = Absinthe.Schema.to_sdl(SDLWithKeyFieldsSchema)
assert sdl =~ "_entities(representations: [_Any!]!): [_Entity]!"
end

defmodule SDLWithoutKeyFieldsSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

query do
field :test, :string
end

object :user do
field :id, non_null(:id)
end
end

test "does not render in sdl without @key" do
sdl = Absinthe.Schema.to_sdl(SDLWithoutKeyFieldsSchema)
refute sdl =~ "_entities(representations: [_Any!]!): [_Entity]!"
end
end
end
Loading

0 comments on commit dc70980

Please sign in to comment.