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. The function should return the entity, or `nil` when not found. - `new_name`: the new name to push the loaded entity into. (optional, defaults to the original argument name). ## Examples ``` query do field :user, :user do arg(:id, :id) # Add the middleware before your resolver middleware( ArgLoader, %{ id: [ new_name: :user, is_list: true, 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`. `ArgLoader` can also be used to load a `list_of` arguments: ``` query do field :users, non_null(list_of(:user)) do arg(:ids, non_null(list_of(:id))) middleware( ArgLoader, %{ ids: [ new_name: :users, load_function: fn ids -> ids |> get_users_by_id() |> AbsintheUtils.Helpers.Sorting.sort_alike(ids, & &1.id) end ] } ) resolve(fn _, params, _ -> {:ok, Map.get(params, :users)} end) end end ``` Note the use of ` AbsintheUtils.Helpers.Sorting.sort_alike/2` to ensure the returned list of entities from the repository is sorted according to the user's input. """ @behaviour Absinthe.Middleware alias AbsintheUtils.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 entities when is_list(entities) -> # TODO: Can we assume the result is a list of entities # based on the output of the load function? # We could also check the input type, # but the saver solution might be to to add a `is_list` option. entities = Enum.reject(entities, &is_nil/1) if is_list(input_value) and length(entities) != length(input_value) do :not_found else Map.put(arguments, push_to_key, entities) end value -> Map.put(arguments, push_to_key, value) end end def load_entities(arguments, _argument_identifier, _opts) do arguments end end