lib/datocms/graphql_client/backends/standard_client.ex

defmodule DatoCMS.GraphQLClient.Backends.StandardClient do
  @moduledoc """
  A basic client for DatoCMS GraphQL requests
  """

  @endpoint "https://graphql.datocms.com/"
  @per_page 100

  def configure(options \\ []) do
    config = configuration(options)

    if Keyword.has_key?(config, :endpoint) do
      endpoint = config[:endpoint]
      Neuron.Config.set(url: endpoint)
    end

    if Keyword.has_key?(config, :api_key) do
      api_key = config[:api_key]
      Neuron.Config.set(headers: [authorization: "Bearer #{api_key}"])
    end

    Neuron.Config.set(connection_opts: [timeout: :infinity, recv_timeout: :infinity])
    Neuron.Config.set(parse_options: [keys: :atoms])
  end

  def configuration(options \\ []) do
    config_defaults = Application.get_env(:datocms_graphql_client, :config, [])
    defaults = Keyword.merge([endpoint: @endpoint], config_defaults)
    Keyword.merge(defaults, options)
  end

  def live? do
    configuration()[:live]
  end

  @doc """
  * params - an optional Map of query parameters
  * options - an optional Keyword of options which are passed on to Neuron
     * url - the endpoint (must be set here, or via `configure/1`)
     * headers - a keyword of request headers
  """
  def query(query, params \\ %{}, options \\ []) do
    Neuron.query(query, params, options)
    |> handle_response()
  end

  def query!(query, params \\ %{}, options \\ []) do
    case query(query, params, options) do
      {:ok, page} -> page
      {:error, error} ->
        raise """
          GraphQL query '#{query}':
          params: #{inspect(params)}
          error: #{inspect(error)}
        """
    end
  end

  def fetch!(key, query, params \\ %{}, options \\ []) do
    case fetch(key, query, params, options) do
      {:ok, page} -> page
      {:error, error} ->
        raise """
          GraphQL query #{key} '#{query}':
          params: #{inspect(params)}
          error: #{inspect(error)}
        """
    end
  end

  def fetch(key, query_body, params \\ %{}, options \\ [])

  def fetch(key, query_body, params, options) when is_atom(key) do
    keyed = fetch_query(key, query_body)
    result = query(keyed, params, options)

    case result do
      {:ok, data} -> {:ok, data[key]}
      other -> other
    end
  end

  def fetch(_key, _query_body, _params, _options) do
    raise "#{__MODULE__}.fetch/4 - first 'key' parameter must be an Atom"
  end

  def fetch_query(key, query), do: "query { #{key} #{query} }"

  def fetch_localized!(key, locale, query, params \\ %{}, options \\ []) do
    case fetch_localized(key, locale, query, params, options) do
      {:ok, page} -> page
      {:error, error} ->
        raise """
          GraphQL query '#{key}':
          '#{query}'
          locale: '#{locale}'
          params: #{inspect(params)}
          error: #{inspect(error)}
        """
    end
  end

  def fetch_localized(key, locale, query, params \\ %{}, options \\ []) do
    keyed = """
      query FetchLocalized($locale: SiteLocale!) {
        #{key}(locale: $locale) #{query}
      }
    """
    with_locale = with_locale(params, locale)

    result = Neuron.query(keyed, with_locale, options)
    |> handle_response()

    case result do
      {:ok, data} -> data[key]
      other -> other
    end
  end

  def fetch_all!(key, query, params \\ %{}, options \\ []) do
    case fetch_all(key, query, params, options) do
      {:ok, pages} -> pages
      {:error, error} ->
        raise """
          GraphQL query '#{key}':
          '#{query}'
          params: #{inspect(params)}
          error: #{inspect(error)}
        """
    end
  end

  def fetch_all(key, query, params \\ %{}, options \\ []) do
    paginated = """
      query paginated($first: IntType!, $skip: IntType!) {
        #{key}(first: $first, skip: $skip) #{query}
      }
    """
    do_fetch_all(key, paginated, with_default_pagination(params), options)
  end

  def fetch_all_localized!(key, locale, query, params \\ %{}, options \\ []) do
    case fetch_all_localized(key, locale, query, params, options) do
      {:ok, pages} -> pages
      {:error, error} ->
        raise """
          GraphQL query '#{key}':
          '#{query}'
          locale: '#{locale}'
          params: #{inspect(params)}
          error: #{inspect(error)}
        """
    end
  end

  def fetch_all_localized(key, locale, query, params \\ %{}, options \\ []) do
    paginated = """
      query paginated($locale: SiteLocale!, $first: IntType!, $skip: IntType!) {
        #{key}(locale: $locale, first: $first, skip: $skip) #{query}
      }
    """
    do_fetch_all(key, paginated, with_locale(with_default_pagination(params), locale), options)
  end

  def do_fetch_all(key, paginated, params, options) do
    Neuron.query(paginated, params, options)
    |> handle_fetch_all_query(key, paginated, params, options)
  end

  defp handle_response({:error, %HTTPoison.Error{id: nil, reason: :nxdomain}}) do
    {:error, "Cannot resolve domain"}
  end
  defp handle_response({:error, %HTTPoison.Error{id: nil, reason: reason}}) do
    {:error, "Failed to connect, reason: #{reason}"}
  end
  defp handle_response({:error, %Neuron.Response{body: %{data: data}}}) do
    {:error, "Request failed: #{inspect(data)}"}
  end
  defp handle_response({:ok, %Neuron.Response{body: %{errors: errors}}}) do
    {:error, errors}
  end
  defp handle_response({:ok, %Neuron.Response{body: %{data: data}}}) do
    {:ok, data}
  end
  defp handle_response({:ok, %Neuron.Response{body: %{url: url}}}) do
    {:ok, url}
  end

  defp handle_fetch_all_query({:error, _errors} = response, _key, _paginated, _params, _options), do: response
  defp handle_fetch_all_query({:ok, response}, key, paginated, params, options) do
    handle_fetch_all_response(response.body, key, paginated, params, options)
  end

  defp handle_fetch_all_response(%{errors: errors} = _body, _key, _paginated, _params, _options) do
    {:error, errors}
  end
  defp handle_fetch_all_response(body, key, paginated, params, options) do
    page = body[:data][key]
    case page do
      [] -> {:ok, []}
      _  ->
        params = Map.put(params, :skip, params.skip + params.first)
        {:ok, pages} = do_fetch_all(key, paginated, params, options)
        {:ok, page ++ pages}
    end
  end

  defp with_default_pagination(params) do
    skip = params[:skip] || 0
    first = params[:first] || @per_page
    params
      |> Map.put(:skip, skip)
      |> Map.put(:first, first)
  end

  defp with_locale(params, locale), do: Map.put(params, :locale, locale)
end