Skip to content

Commit

Permalink
Remove dependency on Ecto to make tests easier
Browse files Browse the repository at this point in the history
  • Loading branch information
maciej-szlosarczyk committed Dec 19, 2024
1 parent 893e93b commit 875b535
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 125 deletions.
13 changes: 0 additions & 13 deletions config/config.exs

This file was deleted.

116 changes: 55 additions & 61 deletions lib/absinthe/federation/schema/entities_field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
resolution_acc = run_callbacks(resolution_acc.schema.plugins(), :before_resolution, resolution_acc)

representations_to_resolve =
Enum.map(resolution.arguments.representations, &resolve_reference_field(&1, resolution_acc, resolution))
Enum.map(resolution.arguments.representations, &resolve_reference_field(&1, resolution_acc))

# Resolve representations first time
resolution_acc = Enum.reduce(representations_to_resolve, resolution_acc, &resolve_representation/2)
Expand Down Expand Up @@ -165,8 +165,6 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
local_res =
representation
|> Map.put(:context, acc.context)
|> Map.put(:value, nil)
|> Map.put(:errors, [])
|> Map.put(:acc, acc.acc)

result = reduce_resolution(local_res)
Expand All @@ -179,78 +177,46 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
end
end

defp resolve_reference_field(representation, persisted_fields, resolution) do
defp resolve_reference_field(representation, resolution_acc) do
typename = Map.get(representation, "__typename")
%{schema: schema, source: source} = persisted_fields
%{schema: schema, source: source, context: context} = resolution_acc
args = convert_keys_to_atom(representation, context)

field =
schema
|> Absinthe.Schema.lookup_type(typename)
|> resolve_representation(source, representation)

schema
|> Absinthe.Schema.lookup_type(typename)
|> resolve_representation(source, representation, resolution)
field
|> Map.put(:arguments, args)
|> Map.put(:schema, schema)
|> Map.put(:source, source)
|> Map.put(:state, resolution.state)
|> Map.put(:state, :unresolved)
|> Map.put(:value, nil)
|> Map.put(:errors, [])
end

defp run_callbacks(plugins, callback, acc) do
Enum.reduce(plugins, acc, &apply(&1, callback, [&2]))
end

defp reduce_resolution(%{middleware: []} = res), do: res

defp reduce_resolution(%{middleware: [middleware | remaining_middleware]} = res) do
case call_middleware(middleware, %{res | middleware: remaining_middleware}) do
%{state: :suspended} = res ->
res

res ->
reduce_resolution(res)
end
end

defp call_middleware({{mod, fun}, opts}, res) do
apply(mod, fun, [res, opts])
end

defp call_middleware({mod, opts}, res) do
apply(mod, :call, [res, opts])
end

defp call_middleware(fun, res) when is_function(fun, 2) do
fun.(res, [])
end

defp resolve_representation(
%struct_type{fields: fields},
parent,
representation,
resolution
)
defp resolve_representation(%struct_type{fields: fields}, parent, _representation)
when struct_type in [Absinthe.Type.Object, Absinthe.Type.Interface] do
resolve_reference(fields[:_resolve_reference], parent, representation, resolution)
resolve_reference(fields[:_resolve_reference], parent)
end

defp resolve_representation(_schema_type, _parent, representation, _schema),
defp resolve_representation(_schema_type, _parent, representation),
do:
{:error,
"The _entities resolver tried to load an entity for type '#{Map.get(representation, "__typename")}', but no object type of that name was found in the schema"}

# When there is no field name _resolve_reference defined on the key object, create
# a stub middleware that returns arguments as the field resolution.
defp resolve_reference(nil, parent, representation, %{context: context} = _resolution) do
args = convert_keys_to_atom(representation, context)
middleware = {{Absinthe.Resolution, :call}, fn args, _res -> {:ok, args} end}
%{arguments: args, value: nil, middleware: [middleware], parent: parent, errors: [], state: :unresolved}
end

# When there is a field _resolve_reference, set it up so the resolution pipeline can be run
# on it.
defp resolve_reference(%{middleware: _middleware} = field, parent, representation, %{context: context}) do
args = convert_keys_to_atom(representation, context)

field
|> Map.put(:arguments, args)
|> Map.put(:parent, parent)
defp resolve_reference(field, parent) do
# When there is a field _resolve_reference, set it up so the resolution pipeline can be run
# on it.
if field do
Map.put(field, :parent, parent)
else
# When there is no field name _resolve_reference defined on the key object, create
# a stub middleware that returns arguments as the field resolution.
middleware = {{Absinthe.Resolution, :call}, fn args, _res -> {:ok, args} end}
%{middleware: [middleware], parent: parent}
end
end

defp convert_keys_to_atom(map, context) when is_map(map) do
Expand All @@ -274,6 +240,34 @@ defmodule Absinthe.Federation.Schema.EntitiesField do
|> String.to_atom()
end

defp run_callbacks(plugins, callback, acc) do
Enum.reduce(plugins, acc, &apply(&1, callback, [&2]))
end

defp reduce_resolution(%{middleware: []} = res), do: res

defp reduce_resolution(%{middleware: [middleware | remaining_middleware]} = res) do
case call_middleware(middleware, %{res | middleware: remaining_middleware}) do
%{state: :suspended} = res ->
res

res ->
reduce_resolution(res)
end
end

defp call_middleware({{mod, fun}, opts}, res) do
apply(mod, fun, [res, opts])
end

defp call_middleware({mod, opts}, res) do
apply(mod, :call, [res, opts])
end

defp call_middleware(fun, res) when is_function(fun, 2) do
fun.(res, [])
end

defp adapter_has_to_internal_name_modifier?(adapter) do
Keyword.get(adapter.__info__(:functions), :to_internal_name) == 2
end
Expand Down
7 changes: 1 addition & 6 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,7 @@ defmodule Absinthe.Federation.MixProject do
{:dataloader, "~> 1.0.9 or ~> 1.0.10 or ~> 2.0"},
# Dev
{:dialyxir, ">= 1.0.0", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.29", only: :dev, runtime: false},

# Ecto Dataloader tests
{:ecto, "~> 3.8", only: [:dev, :test], optional: true},
{:ecto_sql, "~> 3.8", only: [:dev, :test], optional: true},
{:postgrex, ">= 0.0.0", only: [:dev, :test], optional: true}
{:ex_doc, "~> 0.29", only: :dev, runtime: false}
]
end
end
5 changes: 0 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
%{
"absinthe": {:hex, :absinthe, "1.7.8", "43443d12ad2b4fcce60e257ac71caf3081f3d5c4ddd5eac63a02628bcaf5b556", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4085df201892a498384f997649aedb37a4ce8a726c170d5b5617ed3bf45d40b"},
"dataloader": {:hex, :dataloader, "2.0.1", "fa06b057b432b993203003fbff5ff040b7f6483a77e732b7dfc18f34ded2634f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "da7ff00890e1b14f7457419b9508605a8e66ae2cc2d08c5db6a9f344550efa11"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
}
10 changes: 0 additions & 10 deletions priv/example_repo/migrations/20241217112436_add_table.exs

This file was deleted.

37 changes: 19 additions & 18 deletions test/absinthe/federation/schema/entities_field/dataloader_test.exs
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do
use Absinthe.Federation.Case, async: true

setup do
{:ok, repo} = start_supervised(ExampleRepo)

pid = Ecto.Adapters.SQL.Sandbox.start_owner!(ExampleRepo, shared: false)
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
# setup do
# end

example_item =
%ExampleItem{item_id: "1"}
|> ExampleRepo.insert!()
setup do
{:ok, source} = start_supervised(Example.Source)
Example.Source.put(%{"1" => %ExampleItem{item_id: "1"}, "3" => %ExampleItem{item_id: "3"}})

%{repo: repo, example_item: example_item}
%{source: source}
end

describe "resolver with dataloader" do
defmodule EctoDataloaderSchema do
defmodule ExampleDataloaderSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

Expand All @@ -24,7 +21,7 @@ defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(EctoDataloaderSchema, Dataloader.Ecto.new(ExampleRepo))
|> Dataloader.add_source(Example.Source, Dataloader.KV.new(&Example.Source.run_batch/2))

Map.put(ctx, :loader, loader)
end
Expand Down Expand Up @@ -53,8 +50,8 @@ defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do

field :_resolve_reference, :dataloaded_item do
resolve dataloader(
EctoDataloaderSchema,
fn _parent, args, _res -> %{batch: {:one, ExampleItem, %{}}, item: [item_id: args.item_id]} end,
Example.Source,
fn _parent, args, _res -> %{batch: {:one, ExampleItem, %{}}, item: args.item_id} end,
callback: fn item, _parent, _args ->
if item do
item = Map.drop(item, [:__struct__])
Expand All @@ -75,12 +72,12 @@ defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do
field :_resolve_reference, :on_load_item do
resolve fn %{item_id: id}, %{context: %{loader: loader}} ->
batch_key = {:one, ExampleItem, %{}}
item_key = [item_id: id]
item_key = id

loader
|> Dataloader.load(EctoDataloaderSchema, batch_key, item_key)
|> Dataloader.load(Example.Source, batch_key, item_key)
|> on_load(fn loader ->
result = Dataloader.get(loader, EctoDataloaderSchema, batch_key, item_key)
result = Dataloader.get(loader, Example.Source, batch_key, item_key)

if result do
result = Map.drop(result, [:__struct__])
Expand Down Expand Up @@ -109,6 +106,10 @@ defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do
{
__typename: "OnLoadItem",
item_id: "1"
},
{
__typename: "OnLoadItem",
item_id: "2"
}
]) {
...on DataloadedItem {
Expand All @@ -124,8 +125,8 @@ defmodule Absinthe.Federation.Schema.EntitiesField.DataloaderTest do
}
"""

assert {:ok, %{data: %{"_entities" => [%{"item_id" => "1"}, %{"item_id" => "1"}, %{"item_id" => "1"}]}}} =
Absinthe.run(query, EctoDataloaderSchema, variables: %{})
assert {:ok, %{data: %{"_entities" => [%{"item_id" => "1"}, %{"item_id" => "1"}, %{"item_id" => "1"}, nil]}}} =
Absinthe.run(query, ExampleDataloaderSchema, variables: %{})
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Absinthe.Federation.Schema.EntitiesField.FunctionCaptureTest do
use Absinthe.Federation.Case, async: true

describe "resolve a function capture" do
defmodule EctoDataloaderSchema do
defmodule FunctionCaptureSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema

Expand Down Expand Up @@ -53,7 +53,7 @@ defmodule Absinthe.Federation.Schema.EntitiesField.FunctionCaptureTest do
"""

assert {:ok, %{data: %{"_entities" => [%{"item_id" => "11"}]}}} =
Absinthe.run(query, EctoDataloaderSchema, variables: %{})
Absinthe.run(query, FunctionCaptureSchema, variables: %{})
end

test "handles a function capture" do
Expand All @@ -73,7 +73,7 @@ defmodule Absinthe.Federation.Schema.EntitiesField.FunctionCaptureTest do
"""

assert {:ok, %{data: %{"_entities" => [%{"item_id" => "1"}]}}} =
Absinthe.run(query, EctoDataloaderSchema, variables: %{})
Absinthe.run(query, FunctionCaptureSchema, variables: %{})
end
end
end
7 changes: 1 addition & 6 deletions test/support/example_item.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
defmodule ExampleItem do
use Ecto.Schema

schema "example_items" do
field :item_id, :string
timestamps(type: :utc_datetime)
end
defstruct [:item_id]
end
3 changes: 0 additions & 3 deletions test/support/example_repo.ex

This file was deleted.

23 changes: 23 additions & 0 deletions test/support/example_source.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Example.Source do
use Agent

def start_link(initial_value) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
end

def value do
Agent.get(__MODULE__, & &1)
end

def put(value) do
Agent.update(__MODULE__, fn _ -> value end)
end

def run_batch({:one, ExampleItem, %{}}, items) do
value = Agent.get(__MODULE__, & &1)

for item <- items, into: %{} do
{item, Map.get(value, item)}
end
end
end

0 comments on commit 875b535

Please sign in to comment.