Current section

Files

Jump to
absinthe lib absinthe resolution helpers.ex
Raw

lib/absinthe/resolution/helpers.ex

defmodule Absinthe.Resolution.Helpers do
@moduledoc """
Handy functions for returning async or batched resolution functions
Using `Absinthe.Schema.Notation` or (by extension) `Absinthe.Schema` will
automatically import the `batch` and `async` helpers. Dataloader helpers
require an explicit `import Absinthe.Resolution.Helpers` invocation, since
dataloader is an optional dependency.
"""
alias Absinthe.Middleware
@doc """
Execute resolution field asynchronously.
This is a helper function for using the `Absinthe.Middleware.Async`.
Forbidden in mutation fields. (TODO: actually enforce this)
"""
@spec async((() -> term)) :: {:middleware, Middleware.Async, term}
@spec async((() -> term), Keyword.t()) :: {:middleware, Middleware.Async, term}
def async(fun, opts \\ []) do
{:middleware, Middleware.Async, {fun, opts}}
end
@doc """
Batch the resolution of several functions together.
Helper function for creating `Absinthe.Middleware.Batch`
# Example
Raw usage:
```elixir
object :post do
field :name, :string
field :author, :user do
resolve fn post, _, _ ->
batch({__MODULE__, :users_by_id}, post.author_id, fn batch_results ->
{:ok, Map.get(batch_results, post.author_id)}
end)
end
end
end
def users_by_id(_, user_ids) do
users = Repo.all from u in User, where: u.id in ^user_ids
Map.new(users, fn user -> {user.id, user} end)
end
```
"""
@spec batch(Middleware.Batch.batch_fun(), term, Middleware.Batch.post_batch_fun()) ::
{:plugin, Middleware.Batch, term}
@spec batch(
Middleware.Batch.batch_fun(),
term,
Middleware.Batch.post_batch_fun(),
opts :: Keyword.t()
) :: {:plugin, Middleware.Batch, term}
def batch(batch_fun, batch_data, post_batch_fun, opts \\ []) do
batch_config = {batch_fun, batch_data, post_batch_fun, opts}
{:middleware, Middleware.Batch, batch_config}
end
if Code.ensure_loaded?(Dataloader) do
@doc """
Dataloader helper function
This function is not imported by default. To make it available in your module do
```
import Absinthe.Resolution.Helpers
```
This function helps you use data loader in a direct way within your schema.
While normally the `dataloader/1,2,3` helpers are enough, `on_load/2` is useful
when you want to load multiple things in a single resolver, or when you need
fine grained control over the dataloader cache.
## Examples
```elixir
field :reports, list_of(:report) do
resolve fn shipment, _, %{context: %{loader: loader}} ->
loader
|> Dataloader.load(SourceName, :automatic_reports, shipment)
|> Dataloader.load(SourceName, :manaul_reports, shipment)
|> on_load(fn loader ->
reports =
loader
|> Dataloader.get(SourceName, :automatic_reports, shipment)
|> Enum.concat(Dataloader.load(loader, SourceName, :manaul_reports, shipment))
|> Enum.sort_by(&reported_at/1)
{:ok, reports}
end)
end
end
```
"""
def on_load(loader, fun) do
{:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
end
@type dataloader_tuple :: {:middleware, Absinthe.Middleware.Dataloader, term}
@type dataloader_key_fun ::
(Absinthe.Resolution.source(),
Absinthe.Resolution.arguments(),
Absinthe.Resolution.t() ->
{any, map})
@type dataloader_opt :: {:args, map} | {:use_parent, true | false}
@doc """
Resolve a field with a dataloader source.
This function is not imported by default. To make it available in your module do
```
import Absinthe.Resolution.Helpers
```
Same as `dataloader/3`, but it infers the resource name from the field name.
## Examples
```
field :author, :user, resolve: dataloader(Blog)
```
This is identical to doing the following.
```
field :author, :user, resolve: dataloader(Blog, :author, [])
```
"""
@spec dataloader(Dataloader.source_name()) :: dataloader_tuple
def dataloader(source) do
fn parent, args, %{context: %{loader: loader}} = res ->
resource = res.definition.schema_node.identifier
do_dataloader(loader, source, resource, args, parent, [])
end
end
@doc """
Resolve a field with Dataloader
This function is not imported by default. To make it available in your module do
```
import Absinthe.Resolution.Helpers
```
While `on_load/2` makes using dataloader directly easy within a resolver function,
it is often unnecessary to need this level of direct control.
The `dataloader/3` function exists to provide a simple API for using dataloader.
It takes the name of a data source, the name of the resource you want to load,
and then a variety of options.
## Basic Usage
```
object :user do
field :posts, list_of(:post),
resolve: dataloader(Blog, :posts, args: %{deleted: false})
field :organization, :organization do
resolve dataloader(Accounts, :organization, use_parent: false)
end
end
```
## Key Functions
Instead of passing in a literal like `:posts` or `:organization` in as the resource,
it is also possible pass in a function:
```
object :user do
field :posts, list_of(:post) do
arg :limit, non_null(:integer)
resolve dataloader(Blog, fn user, args, info ->
args = Map.update!(args, :limit, fn val ->
max(min(val, 20), 0)
end)
{:posts, args}
end)
end
end
```
In this case we want to make sure that the limit value cannot be larger than
`20`. By passing a callback function to `dataloader/2` we can ensure that
the value will fall nicely between 0 and 20.
## Options
- `:args` default: `%{}`. Any arguments you want to always pass into the
`Dataloader.load/4` call. Resolver arguments are merged into this value and,
in the event of a conflict, the resolver arguments win.
- `:use_parent` default: `true`. This option affects whether or not the `dataloader/2`
helper will use any pre-existing value on the parent. IE if you return
`%{author: %User{...}}` from a blog post the helper will by default simply use
the pre-existing author. Set it to false if you always want it to load it fresh.
Ultimately, this helper calls `Dataloader.load/4`
using the loader in your context, the source you provide, the tuple `{resource, args}`
as the batch key, and then the parent value of the field
```
def dataloader(source_name, resource) do
fn parent, args, %{context: %{loader: loader}} ->
args = Map.merge(opts[:args] || %{}, args)
loader
|> Dataloader.load(source_name, {resource, args}, parent)
|> on_load(fn loader ->
{:ok, Dataloader.get(loader, source_name, {resource, args}, parent)}
end)
end
```
"""
def dataloader(source, fun, opts \\ [])
@spec dataloader(Dataloader.source_name(), dataloader_key_fun | any, [dataloader_opt]) ::
dataloader_tuple
def dataloader(source, fun, opts) when is_function(fun, 3) do
fn parent, args, %{context: %{loader: loader}} = res ->
{resource, args} = fun.(parent, args, res)
do_dataloader(loader, source, resource, args, parent, opts)
end
end
def dataloader(source, resource, opts) do
fn parent, args, %{context: %{loader: loader}} ->
do_dataloader(loader, source, resource, args, parent, opts)
end
end
defp use_parent(loader, source, resource, parent, args, opts) do
with true <- Keyword.get(opts, :use_parent, false),
{:ok, val} <- is_map(parent) && Map.fetch(parent, resource) do
Dataloader.put(loader, source, {resource, args}, parent, val)
else
_ -> loader
end
end
defp do_dataloader(loader, source, resource, args, parent, opts) do
args =
opts
|> Keyword.get(:args, %{})
|> Map.merge(args)
loader
|> use_parent(source, resource, parent, args, opts)
|> Dataloader.load(source, {resource, args}, parent)
|> on_load(fn loader ->
result = Dataloader.get(loader, source, {resource, args}, parent)
{:ok, result}
end)
end
end
end