lib/absinthe_remote/remote_schema.ex

defmodule AbsintheRemote.RemoteSchema do
  @callback resolve_query(
              query :: binary(),
              operation_name :: binary(),
              variables :: map()
            ) ::
              {:ok, data :: map()} | {:error, reason :: term}
  defmacro __using__(_opts) do
    quote do
      use Absinthe.Schema

      @behaviour AbsintheRemote.RemoteSchema

      def hydrate(%Absinthe.Blueprint.Schema.FieldDefinition{identifier: query}, [
            %Absinthe.Blueprint.Schema.ObjectTypeDefinition{identifier: :query} | _
          ]) do
        case Atom.to_string(query) do
          "__" <> _internal ->
            # If this is an internal query (__type, __schema, etc), let it do its thing
            []

          _ ->
            {:resolve, &__MODULE__.get_value/3}
        end
      end

      def hydrate(_node, _ancestors) do
        []
      end

      def get_value(one, variables, %Absinthe.Resolution{definition: query} = resolution) do
        query_variables =
          Enum.map(query.arguments, &argument_to_query_variable/1) |> Enum.into(%{})

        case resolve_query(
               resolution.private.raw_source,
               resolution.private.operation_name,
               query_variables
             ) do
          {:ok, result} ->
            # pop the value out of the inner struct,
            # of if it doesn't exist, just use the result
            {:ok, Map.get(result, String.to_atom(query.name), result)}

          other ->
            other
        end
      end

      defp flatten_errors([%{"message" => message} | rest], acc) do
        flatten_errors(rest, acc ++ [message])
      end

      defp flatten_errors(_, acc), do: acc

      defp keys_to_atoms(string_key_map) when is_map(string_key_map) do
        for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)}
      end

      defp keys_to_atoms(value), do: value

      defp argument_to_query_variable(%Absinthe.Blueprint.Input.Argument{} = arg) do
        # Ideally grab the key from
        key = fetch_key(arg)
        value = fetch_normalized_value(arg.input_value.normalized)

        if key do
          {key, value}
        else
          {value}
        end
      end

      defp fetch_key(%Absinthe.Blueprint.Input.Argument{
             input_value: %Absinthe.Blueprint.Input.Value{
               raw: %Absinthe.Blueprint.Input.RawValue{
                 content: %Absinthe.Blueprint.Input.Variable{name: name}
               }
             }
           }),
           do: name

      defp fetch_key(%Absinthe.Blueprint.Input.Argument{name: name}), do: name

      defp fetch_normalized_value(%Absinthe.Blueprint.Input.Null{}), do: nil

      defp fetch_normalized_value(some_input) do
        if Map.has_key?(some_input, :value) do
          some_input.value
        else
          raise "Unknown input type"
        end
      end
    end
  end
end