lib/graphql/query_registry.ex

defmodule GraphQL.QueryRegistry do
  @moduledoc """
  Functions to handle query registries.

  A query registry stores several `GraphQL.Query` structs, so they
  can be combined into a single query before the execution.
  """
  alias GraphQL.{Client, Query}

  @enforce_keys [:name]
  defstruct name: nil, queries: [], variables: [], resolvers: []

  @typedoc """
  A resolver is a function that must accept two arguments:
    - a `GraphQL.Response` struct
    - an accumulator, that can be of any type

  It also must return the updated value of the accumulator.
  """
  @type resolver :: (Response.t(), any() -> any())

  @typedoc """
  A struct that keeps the information about several queries, variables and
  resolvers.

  The `name` field will be used as the name of the final query or mutation.

  The `queries` field is a list of `GraphQL.Query` structs, that
  will be merged before execution.

  The `variables` is a map with all _values_ of variables that will be sent
  to the server along with the GraphQL body.

  The `resolver` is a list of `t:resolver()` functions that can be used to
  produce the side effects in an accumulator.
  """
  @type t :: %__MODULE__{
          name: String.t(),
          queries: [Query.t()],
          variables: [map()],
          resolvers: list()
        }

  @doc """
  Creates a new QueryRegistry struct with the given name.
  """
  @spec new(String.t()) :: t()
  def new(name) do
    %__MODULE__{name: name}
  end

  @doc """
  Add a query to the a query registry
  """
  @spec add_query(t(), Query.t(), map()) :: t()
  def add_query(%__MODULE__{} = registry, %Query{} = query, variables \\ nil) do
    updated_variables =
      if variables == %{} || variables == nil do
        registry.variables
      else
        [variables | registry.variables]
      end

    %__MODULE__{registry | queries: [query | registry.queries], variables: updated_variables}
  end

  @doc """
  Add a new resolver into a query registry
  """
  @spec add_resolver(t(), resolver()) :: t()
  def add_resolver(%__MODULE__{} = registry, function) when is_function(function, 2) do
    add_resolvers(registry, [function])
  end

  @doc """
  Add a list of resolvers into a query registry
  """
  @spec add_resolvers(t(), [resolver()]) :: t()
  def add_resolvers(%__MODULE__{} = registry, resolvers) do
    %__MODULE__{registry | resolvers: registry.resolvers ++ resolvers}
  end

  @doc """
  Executes the given query registry, using the given accumulator `acc` and the given options
  """
  @spec execute(t(), any(), Keyword.t()) :: any()
  def execute(registry, acc, options \\ []) do
    case prepare_query(registry) do
      {:ok, {query, variables, resolvers}} ->
        result =
          query
          |> Client.execute(variables, options)
          |> resolve(resolvers, acc)

        {:ok, result}

      error ->
        error
    end
  end

  defp prepare_query(%__MODULE__{} = registry) do
    case registry.queries do
      [] ->
        {:error, "no queries available"}

      _not_empty ->
        case Query.merge_many(registry.queries, registry.name) do
          {:ok, query} ->
            variables = merge_variables(registry.variables)
            {:ok, {query, variables, registry.resolvers}}

          error ->
            error
        end
    end
  end

  defp merge_variables([]), do: %{}

  defp merge_variables(variables) do
    Enum.reduce(variables, &Map.merge/2)
  end

  defp resolve(response, resolvers, initial_acc) do
    Enum.reduce(resolvers, initial_acc, fn resolver, acc ->
      resolver.(response, acc)
    end)
  end
end