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] ?
Unfortunately, there is no way to define this at the input_object level. However, there are a few different ways to handle parameter transformation.
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
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
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
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.
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.