lib/graphql/query.ex

defmodule GraphQL.Query do
  @moduledoc """
  Functions to create and modify query representations.
  """
  alias GraphQL.{Node, Variable}

  @enforce_keys [:operation, :name, :fields]
  defstruct [:operation, :name, :fields, :fragments, :variables]

  @typedoc """
  A struct that represents a GraphQL query or mutation.

  The `:operation` field can be `:query`, for a query operation, or `:mutation`,
  for a mutation operation.

  The `:name` field is the name of the query or mutation. GraphQL does not
  require a name for operations, but this struct will enforce its presence in
  order to enrich trace and logging information.

  The `:fields` field is a list of `GraphQL.Node` structs. This the
  list of roof fields of a query or mutation.

  The `:fragments` field is also a list of `GraphQL.Node` structs,
  but intended to only keep fragment nodes, as they are usually placed after
  the  root fields in a typical GraphQL query/mutation.

  The `:variables` fields is a list of `GraphQL.Variable` structs,
  that represents the expected variables during the request. Note that this list
  is the _definition_ of variables, not the _values_ of them.
  """
  @type t :: %__MODULE__{
          operation: :query | :mutation,
          name: String.t(),
          fields: [Node.t()],
          fragments: [Node.t()] | nil,
          variables: [Variable.t()] | nil
        }

  @doc """
  Creates a new query struct for a 'query' operation from a keyword list.
  """
  @spec query(Keyword.t()) :: t()
  def query(options) do
    options = Keyword.put(options, :operation, :query)
    struct(__MODULE__, options)
  end

  @doc """
  Creates a new query struct for a 'mutation' operation from a keyword list.
  """
  @spec mutation(Keyword.t()) :: t()
  def mutation(options) do
    options = Keyword.put(options, :operation, :mutation)
    struct(__MODULE__, options)
  end

  @doc """
  Adds a field to a query.

  The `field` argument must be a `GraphQL.Node` struct and its
  `:node_type` must be `:field`.

  ## Examples

      iex> f1 = GraphQL.Node.field(:field)
      %GraphQL.Node{node_type: :field, name: :field}
      iex> f2 = GraphQL.Node.field(:other_field)
      %GraphQL.Node{node_type: :field, name: :other_field}
      iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [f1]}
      %GraphQL.Query{operation: :query, name: "MyQuery", fields: [f1]}
      iex> add_field(q, f2)
      %GraphQL.Query{name: "MyQuery", operation: :query, fields: [f2, f1]}

  """
  @spec add_field(t(), Node.t()) :: t()
  def add_field(%__MODULE__{fields: fields} = query, %Node{node_type: :field} = field) do
    fields = if(fields == nil, do: [], else: fields)
    %__MODULE__{query | fields: [field | fields]}
  end

  @doc """
  Adds a fragment to a query.

  The `field` argument must be a `GraphQL.Node` struct and its
  `:node_type` must be `:field`.

  ## Examples

      iex> f1 = GraphQL.Node.fragment("personFields", "Person", [GraphQL.Node.field(:field)])
      %GraphQL.Node{node_type: :fragment, name: "personFields", type: "Person", nodes: [%GraphQL.Node{node_type: :field, name: :field}]}
      iex> f2 = GraphQL.Node.fragment("userFields", "User", [GraphQL.Node.field(:another_field)])
      %GraphQL.Node{node_type: :fragment, name: "userFields", type: "User", nodes: [%GraphQL.Node{node_type: :field, name: :another_field}]}
      iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], fragments: [f1]}
      %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], fragments: [f1]}
      iex> add_fragment(q, f2)
      %GraphQL.Query{name: "MyQuery", operation: :query, fields: [], fragments: [f2, f1]}

  """
  @spec add_fragment(t(), Node.t()) :: t()
  def add_fragment(
        %__MODULE__{fragments: fragments} = query,
        %Node{node_type: :fragment} = fragment
      ) do
    fragments = if(fragments == nil, do: [], else: fragments)
    %__MODULE__{query | fragments: [fragment | fragments]}
  end

  @doc """
  Add a new variable to an existing query

  ## Examples

      iex> v1 = %GraphQL.Variable{name: "id", type: "Integer"}
      %GraphQL.Variable{name: "id", type: "Integer"}
      iex> v2 = %GraphQL.Variable{name: "slug", type: "String"}
      %GraphQL.Variable{name: "slug", type: "String"}
      iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v1]}
      %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v1]}
      iex> add_variable(q, v2)
      %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v2, v1]}
  """
  @spec add_variable(t(), Variable.t()) :: t()
  def add_variable(%__MODULE__{variables: variables} = query, %Variable{} = variable) do
    variables = if(variables == nil, do: [], else: variables)
    %__MODULE__{query | variables: [variable | variables]}
  end

  @doc """
  Combine two queries into one query, merging fields, variables and fragments.

  The two queries must have the same operation.
  """
  @spec merge(t(), t(), String.t()) :: {:ok, t()} | {:error, any()}
  def merge(
        %__MODULE__{operation: operation} = query_a,
        %__MODULE__{operation: operation} = query_b,
        name
      ) do
    with {:ok, variables} <- merge_variables(query_a.variables || [], query_b.variables || []) do
      {:ok,
       %__MODULE__{
         name: name,
         operation: operation,
         fields: (query_a.fields || []) ++ (query_b.fields || []),
         fragments: (query_a.fragments || []) ++ (query_b.fragments || []),
         variables: variables
       }}
    else
      error -> error
    end
  end

  defp merge_variables(set_a, set_b) do
    repeated_vars =
      for v_a <- set_a, v_b <- set_b, reduce: [] do
        acc ->
          if GraphQL.Variable.same?(v_a, v_b) do
            [v_a | acc]
          else
            acc
          end
      end

    case repeated_vars do
      [] ->
        {:ok, set_a ++ set_b}

      _ ->
        var_names =
          repeated_vars
          |> Enum.map(&"\"#{&1.name}\"")
          |> Enum.join(", ")

        {:error, "variables declared twice: #{var_names}"}
    end
  end

  @doc """
  Combines a list of queries into one query, merging fields, variables and fragments.

  All queries must have the same operation.
  """
  @spec merge_many([t()], String.t()) :: {:ok, t()} | {:error, any()}
  def merge_many(queries, name \\ nil)

  def merge_many([%__MODULE__{} = query], name) do
    if name != nil do
      {:ok, %__MODULE__{query | name: name}}
    else
      {:ok, query}
    end
  end

  def merge_many([first_query | remaining_queries], name) do
    result =
      Enum.reduce_while(remaining_queries, first_query, fn query, result ->
        case merge(query, result, name) do
          {:ok, merged_query} ->
            {:cont, merged_query}

          {:error, error} ->
            {:halt, {:error, error}}
        end
      end)

    case result do
      %__MODULE__{} = query -> {:ok, query}
      error -> error
    end
  end
end