Packages
ash_graphql
0.9.4
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.5
1.0.0-rc.4
retired
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.9
0.26.8
0.26.7
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.0
0.25.13
0.25.12
0.25.10
0.25.9
0.25.8
0.25.7
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.13
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
retired
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0-rc.3
0.20.0-rc.2
0.20.0-rc.1
0.20.0-rc.0
0.19.0
0.18.0-rc0
0.17.5
0.17.5-rc0
0.17.4
0.17.2
0.17.1
0.17.0
0.16.28
0.16.27
0.16.26
0.16.25
0.16.24
0.16.23
0.16.22
0.16.21
0.16.20
0.16.18-rc5
0.16.18-rc4
0.16.18-rc3
0.16.18-rc2
0.16.18-rc1
0.16.18-rc0
0.16.17
0.16.16
0.16.15
0.16.14
0.16.13
0.16.12
0.16.11
0.16.10
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.5
0.12.4
0.12.3
0.12.1
0.12.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
The extension for building GraphQL APIs with Ash
Current section
Files
Jump to
Current section
Files
lib/graphql/dataloader.ex
defmodule AshGraphql.Dataloader do
@moduledoc "The dataloader in charge of resolving "
defstruct [
:api,
batches: %{},
results: %{},
default_params: %{}
]
@type t :: %__MODULE__{
api: Ash.api(),
batches: map,
results: map,
default_params: map
}
@type api_opts :: Keyword.t()
@type batch_fun :: (Ash.resource(), Ash.query(), any, [any], api_opts -> [any])
@doc """
Create an Ash Dataloader source.
This module handles retrieving data from Ash for dataloader. It requires a
valid Ash API.
"""
@spec new(Ash.api()) :: t
def new(api) do
%__MODULE__{api: api}
end
defimpl Dataloader.Source do
def run(source) do
results = Dataloader.async_safely(__MODULE__, :run_batches, [source])
results =
Map.merge(source.results, results, fn _, {:ok, v1}, {:ok, v2} ->
{:ok, Map.merge(v1, v2)}
end)
%{source | results: results, batches: %{}}
end
def fetch(source, batch_key, item) do
{batch_key, item_key, _item} =
batch_key
|> normalize_key(source.default_params)
|> get_keys(item)
case Map.fetch(source.results, batch_key) do
{:ok, batch} ->
fetch_item_from_batch(batch, item_key)
:error ->
{:error, "Unable to find batch #{inspect(batch_key)}"}
end
end
defp fetch_item_from_batch({:error, _reason} = tried_and_failed, _item_key),
do: tried_and_failed
defp fetch_item_from_batch({:ok, batch}, item_key) do
case Map.fetch(batch, item_key) do
:error -> {:error, "Unable to find item #{inspect(item_key)} in batch"}
result -> result
end
end
def put(source, _batch, _item, %Ash.NotLoaded{type: :relationship}) do
source
end
def put(source, batch, item, result) do
batch = normalize_key(batch, source.default_params)
{batch_key, item_key, _item} = get_keys(batch, item)
results =
Map.update(
source.results,
batch_key,
{:ok, %{item_key => result}},
fn {:ok, map} -> {:ok, Map.put(map, item_key, result)} end
)
%{source | results: results}
end
def load(source, batch, item) do
{batch_key, item_key, item} =
batch
|> normalize_key(source.default_params)
|> get_keys(item)
if fetched?(source.results, batch_key, item_key) do
source
else
entry = {item_key, item}
update_in(source.batches, fn batches ->
Map.update(batches, batch_key, MapSet.new([entry]), &MapSet.put(&1, entry))
end)
end
end
defp fetched?(results, batch_key, item_key) do
case results do
%{^batch_key => {:ok, %{^item_key => _}}} -> true
_ -> false
end
end
def pending_batches?(%{batches: batches}) do
batches != %{}
end
def timeout(_) do
Dataloader.default_timeout()
end
defp related(path, resource) do
Ash.Resource.related(resource, path) ||
raise """
Valid relationship for path #{inspect(path)} not found on resource #{inspect(resource)}
"""
end
defp get_keys({assoc_field, opts}, %resource{} = record) when is_atom(assoc_field) do
validate_resource(resource)
pkey = Ash.Resource.primary_key(resource)
id = Enum.map(pkey, &Map.get(record, &1))
queryable = related([assoc_field], resource)
{{:assoc, resource, self(), assoc_field, queryable, opts}, id, record}
end
defp get_keys(key, item) do
raise """
Invalid: #{inspect(key)}
#{inspect(item)}
The batch key must either be a schema module, or an association name.
"""
end
defp validate_resource(resource) do
unless Ash.Resource.resource?(resource) do
raise "The given module - #{resource} - is not an Ash resouce."
end
end
defp normalize_key({key, params}, default_params) do
{key, Enum.into(params, default_params)}
end
defp normalize_key(key, default_params) do
{key, default_params}
end
def run_batches(source) do
options = [
timeout: Dataloader.default_timeout(),
on_timeout: :kill_task
]
results =
source.batches
|> Task.async_stream(
fn batch ->
id = :erlang.unique_integer()
system_time = System.system_time()
start_time_mono = System.monotonic_time()
emit_start_event(id, system_time, batch)
batch_result = run_batch(batch, source)
emit_stop_event(id, start_time_mono, batch)
batch_result
end,
options
)
|> Enum.map(fn
{:ok, {_key, result}} -> {:ok, result}
{:exit, reason} -> {:error, reason}
end)
source.batches
|> Enum.map(fn {key, _set} -> key end)
|> Enum.zip(results)
|> Map.new()
end
defp run_batch(
{{:assoc, source_resource, _pid, field, _resource, opts} = key, records},
source
) do
{ids, records} = Enum.unzip(records)
query = opts[:query]
api_opts = opts[:api_opts]
tenant = opts[:tenant] || tenant_from_records(records)
empty = source_resource |> struct |> Map.fetch!(field)
records = records |> Enum.map(&Map.put(&1, field, empty))
cardinality = Ash.Resource.relationship(source_resource, field).cardinality
query =
query
|> Ash.Query.new()
|> Ash.Query.set_tenant(tenant)
loaded = source.api.load!(records, [{field, query}], api_opts || [])
results =
case cardinality do
:many ->
Enum.map(loaded, fn record ->
List.wrap(Map.get(record, field))
end)
:one ->
Enum.map(loaded, fn record ->
Map.get(record, field)
end)
end
{key, Map.new(Enum.zip(ids, results))}
end
defp tenant_from_records([%{__metadata__: %{tenant: tenant}}]) when not is_nil(tenant) do
tenant
end
defp tenant_from_records(_), do: nil
defp emit_start_event(id, system_time, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :start],
%{system_time: system_time},
%{id: id, batch: batch}
)
end
defp emit_stop_event(id, start_time_mono, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :stop],
%{duration: System.monotonic_time() - start_time_mono},
%{id: id, batch: batch}
)
end
end
end