lib/middleware/arg_loader.ex

defmodule AbsintheUtils.Middleware.ArgLoader do
  @moduledoc """
  Absinthe middleware for loading entities in `field` arguments.

  This middleware should be defined before `resolve`. It will manipulate the arguments
  before they are passed to the resolver function.

  As configuration it accepts a map of original argument names to a keyword list, containing:

  - `load_function`: the function used to load the argument into an entity.
    As an input accepts one single argument: the input received in the resolution.
    It returns a tuple of `{:ok, nil}`  `{:ok, nil}` o
  - `new_name`: the new name to push the loaded entity into.
    (optional, defaults to the original argument name).


  ## Example

  ```
  query do
    field :user, :user do
      arg(:id, :id)

      middleware(
        ArgLoader,
        %{
          id: [
            new_name: :user,
            load_function: &get_user_by_id/1
          ]
        }
      )

      resolve(fn _, arguments, _ ->
        {:ok, Map.get(arguments, :user)}
      end)
    end
  ```

  This will define a `user` query that accepts an `id` input. Before calling the resolver,
  it will load the user via `get_user_by_id(id)` into the `user` argument, removing `id`.

  """

  @behaviour Absinthe.Middleware

  alias AbsintheUtilsTest.Helpers.Errors

  @impl true
  def call(
        resolution = %{arguments: arguments},
        opts
      ) do
    {arguments, not_found_arguments} =
      opts
      |> Enum.reduce(
        {arguments, []},
        fn {argument_name, opts}, {arguments, not_found_arguments} ->
          case load_entities(
                 arguments,
                 argument_name,
                 opts
               ) do
            :not_found ->
              {
                arguments,
                [argument_name | not_found_arguments]
              }

            arguments ->
              {
                arguments,
                not_found_arguments
              }
          end
        end
      )

    if Enum.empty?(not_found_arguments) do
      %{
        resolution
        | arguments: arguments
      }
    else
      Errors.put_error(
        resolution,
        "The entity(ies) provided in the following arg(s), could not be found: " <>
          Enum.join(not_found_arguments, ", "),
        "NOT_FOUND"
      )
    end
  end

  def load_entities(arguments, argument_name, opts)
      when is_map_key(arguments, argument_name) do
    load_function = Keyword.fetch!(opts, :load_function)
    push_to_key = Keyword.get(opts, :new_name, argument_name)

    {input_value, arguments} = Map.pop!(arguments, argument_name)

    case load_function.(input_value) do
      nil ->
        :not_found

      values when is_list(values) ->
        # FIXME: we should not assume a list based on the output of the load function
        # WE could also check the input type, but it would be better to replace it with a configuration flag
        if length(values) != length(input_value) do
          :not_found
        else
          Map.put(arguments, push_to_key, values)
        end

      value ->
        Map.put(arguments, push_to_key, value)
    end
  end

  def load_entities(arguments, _argument_identifier, _opts) do
    arguments
  end
end