2

Is there some way to map an input_object into a single data structure?

  input_object(:numrange) do
    field :start, non_null(:integer) do
      resolve(fn field, _, _ ->
        ...
      end)
    end
    field :end, non_null(:integer) do
      resolve(fn field, _, _ ->
        ...
      end)
    end
  end

And then have it parsed as [start, end] ?

atomkirk
  • 3,701
  • 27
  • 30

1 Answers1

3

Unfortunately, there is no way to define this at the input_object level. However, there are a few different ways to handle parameter transformation.

1. Change your business logic

You can just change the contracts within your service layer to handle an object instead of an array.

# Inside of some module

def my_functon(attrs, other, params) do
  formatted_attrs = case attrs do
    %{range: %{start: startrange, end: endrange}} ->
      %{attrs | range: [startrange, endrange]}
    _ -> 
      attrs
  end
  # ...
end

2. Handle the input object where it is passed by args in the schema

You can transform the arguments when they are submitted

# Some graphql definition

field(:my_field, :my_object) do
  arg(:range, non_null(:numrange))
  resolve(fn parent, %{range: %{start: rstart, end: rend}} = args, ctx ->
    new_args = %{args | range: [rstart, rend]}
    SomeModule.my_function(new_attrs, parent, ctx)
  end)
end

3. Create a middleware

You can create an Absinthe.Middleware to transform arguments when they are submitted

defmodule MyApp.NumrangeTransform do
  @behaviour Absinthe.Middleware

  @impl Absinthe.Middleware
  def call(%Absinthe.Resolution{arguments: args} = res, opts) do
    field = Keyword.fetch!(opts, :field)
    new_args = case Map.get(args, field) do
      %{start: rstart, end: rend} ->
         Map.put(args, field, [rstart, rend])
      _ ->
        args
    end

    %{res | arguments: new_args}
  end
end

Then in your schema definition:

field(:my_field, :type) do
  middleware(MyApp.NumrangeTransform, field: :range)
  arg(:range, :numrange)
  # ...
end

The Middleware will transform the args for you without having to write transformation logic everywhere

4. Create a custom scalar type

custom scalar types can be defined in Absinthe:

# In some schema definition

scalar :numrange, name: "NumRange" do
  description("A number range of integers m..n")
  serialize([rstart, rend]) when is_integer(rstart) and is_integer(rend) do
    rstart <> ".." <> rend
  end

  parse(&do_parse/1)

  defp do_parse(%Absinthe.Blueprint.Input.String{value: range_str}) do
    with [s_str, e_str] <- String.split(range_str, ".."),
       {rstart, _} <- Integer.parse(s_str),
       {rend, _} <- Integer.parse(e_str) do
      {:ok, [rstart, rend]}
    else
      _ -> :error
    end
  end

  def do_parse(%Absinthe.Blueprint.Input.Null{}), do: {:ok, nil}
  def do_parse(_), do: :error
end

Then it is added somewhere in the schema

field(:my_field, :type) do
  arg(:range, non_null(:numrange))
  # ...
end

And the GraphQL looks something like this:

query SomeQuery {
  myField(range:"1..3")
}

This is probably the least attractive option as it creates a non-standard way of both presenting and accepting number ranges for any front-end application. However, if this is not a public API that is accessed by third-party applications, there should be no issue with doing it.

Conclusion

There are many ways to define and handle parameter transformation in your input arguments. There may be other solutions that I haven't mentioned. You could probably do something pretty crazy by writing a custom Absinthe.Phase, but that is a complex endeavour that would likely be too heavy-handed for something this simple.

Mike Quinlan
  • 2,873
  • 17
  • 25