defmodule Absinthe.Compose.QueryGenerator do @moduledoc """ Takes an Absinthe resolution struct and generates the relevant GraphQL query and variables. """ alias Absinthe.Blueprint @typename_field %Blueprint.Document.Field{name: "__typename"} def render(%{definition: tree, path: path, fragments: fragments}) do root_operation_node = List.last(path) used_fragments = gather_fragments(tree, fragments) used_variables = gather_variables(tree, used_fragments) query = build_query(tree, root_operation_node, used_variables, used_fragments) {query, used_variables} end defp gather_fragments(tree, fragments) do used_fragments = gather_tree_fragments(tree, fragments) Enum.filter(fragments, fn {name, _fragment} -> name in used_fragments end) |> Enum.into(%{}) end defp gather_tree_fragments(%{selections: selections}, fragments) do Enum.flat_map(selections, &gather_tree_fragments(&1, fragments)) end defp gather_tree_fragments(%Blueprint.Document.Fragment.Spread{name: name}, fragments) do [name | gather_tree_fragments(fragments[name], fragments)] end def gather_variables(tree, fragments) do (gather_tree_variables(tree) ++ gather_fragment_variables(fragments)) |> Enum.into(%{}) end defp gather_tree_variables(%Blueprint.Document.Field{arguments: arguments, selections: []}) do gather_arguments(arguments) end defp gather_tree_variables(%Blueprint.Document.Field{ arguments: arguments, selections: selections }) do argument_names = gather_arguments(arguments) Enum.reduce(selections, argument_names, fn selection, acc -> acc ++ gather_tree_variables(selection) end) end defp gather_tree_variables(%{selections: selections}) do Enum.reduce(selections, [], fn selection, acc -> acc ++ gather_tree_variables(selection) end) end defp gather_tree_variables(_) do [] end defp gather_fragment_variables(fragments) do Enum.reduce(fragments, [], fn {_name, fragment}, acc -> acc ++ gather_tree_variables(fragment) end) end def gather_arguments(arguments) do arguments |> Enum.map(&select_variable_arguments/1) |> List.flatten() |> Enum.reject(&is_nil/1) end defp select_variable_arguments(%{input_value: input_value}) do select_variable_arguments(input_value) end defp select_variable_arguments(%{ raw: %Blueprint.Input.RawValue{content: %Blueprint.Input.Variable{name: name}}, schema_node: type, data: data }) do {name, translate_scalar(data, type)} end defp select_variable_arguments(%{normalized: %Blueprint.Input.List{items: items}}) do Enum.map(items, &select_variable_arguments/1) end defp select_variable_arguments(%{normalized: %Blueprint.Input.Object{fields: fields}}) do Enum.map(fields, &select_variable_arguments/1) end defp select_variable_arguments(_argument) do nil end defp translate_scalar(nil, _type), do: nil defp translate_scalar(internal_value, %Absinthe.Type.NonNull{of_type: sub_type}) do translate_scalar(internal_value, sub_type) end defp translate_scalar(internal_value, %Absinthe.Type.Scalar{identifier: :id}), do: internal_value defp translate_scalar(internal_value, %Absinthe.Type.Scalar{identifier: :string}), do: internal_value defp translate_scalar(internal_value, %Absinthe.Type.Scalar{identifier: :integer}), do: internal_value defp translate_scalar(internal_value, %Absinthe.Type.Enum{} = enum) do enum.values |> Map.fetch!(internal_value) |> Map.fetch!(:name) end defp translate_scalar(internal_value, type) do raise "Not sure how to translate to proxy #{inspect(internal_value)} to #{inspect(type)}" end @indent_increment 2 defp build_query(tree, root_operation_node, used_variables, fragments) do root_operation_type = build_operation_type(root_operation_node) root_operation_name = build_operation_name(root_operation_node) root_operation_variables = build_operation_variables(root_operation_node, used_variables) field_selection = build_field_selection(tree, @indent_increment) spread_fragments = build_fragments(fragments) "#{root_operation_type}#{root_operation_name}#{root_operation_variables} " <> "{\n#{field_selection}\n}#{spread_fragments}" end defp build_operation_type(%Blueprint.Document.Operation{type: operation_type}) do "#{operation_type}" end defp build_operation_name(%Blueprint.Document.Operation{name: nil}) do "" end defp build_operation_name(%Blueprint.Document.Operation{name: operation_name}) do " #{operation_name}" end defp build_operation_variables( %Blueprint.Document.Operation{variable_definitions: variable_definitions}, used_variables ) do used_variable_names = Map.keys(used_variables) variable_definitions |> Enum.filter(&(&1.name in used_variable_names)) |> Enum.map(&build_operation_variable_definition/1) |> Enum.join(", ") |> case do "" -> "" variables -> "(#{variables})" end end defp build_operation_variable_definition(%Blueprint.Document.VariableDefinition{ name: name, type: type }) do "$#{name}: #{build_variable_typename(type)}" end defp build_variable_typename(%Blueprint.TypeReference.NonNull{of_type: of_type}) do build_variable_typename(of_type) <> "!" end defp build_variable_typename(%Blueprint.TypeReference.List{of_type: of_type}) do "[" <> build_variable_typename(of_type) <> "]" end defp build_variable_typename(%Blueprint.TypeReference.Name{name: typename}) do typename end defp build_field_selection( %Blueprint.Document.Fragment.Inline{ selections: children, schema_node: %{name: type_name} }, indent_level ) do indent = String.duplicate(" ", indent_level) subtree = Enum.map(children, &build_field_selection(&1, indent_level + @indent_increment)) |> Enum.join("\n") "#{indent}... on #{type_name} {\n#{subtree}\n#{indent}}" end defp build_field_selection( %Blueprint.Document.Field{selections: []} = blueprint_node, indent_level ) do String.duplicate(" ", indent_level) <> build_field_name(blueprint_node) end defp build_field_selection( %Blueprint.Document.Field{selections: children} = blueprint_node, indent_level ) do indent = String.duplicate(" ", indent_level) field_name = build_field_name(blueprint_node) subtree = [@typename_field | children] |> Enum.map(&build_field_selection(&1, indent_level + @indent_increment)) |> Enum.join("\n") "#{indent}#{field_name} {\n#{subtree}\n#{indent}}" end defp build_field_selection( %Blueprint.Document.Fragment.Spread{name: fragment_name}, indent_level ) do indent = String.duplicate(" ", indent_level) "#{indent}...#{fragment_name}" end defp build_field_name(%Blueprint.Document.Field{name: name, arguments: args}) do args |> exclude_args_with_default_values |> case do [] -> name used_input_args -> "#{name}(#{build_input_args(used_input_args)})" end end defp exclude_args_with_default_values(args) do Enum.reject(args, fn %{ input_value: %Blueprint.Input.Value{ normalized: %Absinthe.Blueprint.Input.Generated{} } } -> true _ -> false end) end defp build_input_args(args) do Enum.map(args, &build_input_arg(&1)) |> Enum.join(", ") end defp build_input_arg(%{ name: arg_name, input_value: input_value }) do "#{arg_name}: #{build_input_value(input_value)}" end defp build_input_value(%Blueprint.Input.Value{ raw: %Blueprint.Input.RawValue{content: %Blueprint.Input.Variable{name: var_name}} }) do "$#{var_name}" end defp build_input_value(%Blueprint.Input.Value{ normalized: %Blueprint.Input.String{value: value} }) do "\"#{value}\"" end defp build_input_value(%Blueprint.Input.Value{ normalized: %Blueprint.Input.Object{fields: sub_fields} }) do used_sub_fields = exclude_args_with_default_values(sub_fields) "{ #{build_input_args(used_sub_fields)} }" end defp build_input_value(%Blueprint.Input.Value{ normalized: %Blueprint.Input.List{items: items} }) do list_items = Enum.map(items, &build_input_value/1) |> Enum.join(", ") "[ #{list_items} ]" end defp build_input_value(%Blueprint.Input.Value{ normalized: %Blueprint.Input.Null{} }) do "null" end defp build_input_value(%Blueprint.Input.Value{normalized: %{value: value}}) do value end defp build_fragments(fragments) do Enum.map(fragments, &build_fragment(&1)) |> Enum.join("") end defp build_fragment( {fragment_name, %{type_condition: %{name: type_name}, selections: children}} ) do subtree = Enum.map(children, &build_field_selection(&1, @indent_increment)) |> Enum.join("\n") "\n\nfragment #{fragment_name} on #{type_name} {\n#{subtree}\n}" end end