absinthe icon indicating copy to clipboard operation
absinthe copied to clipboard

Open ended scalars don't support list values

Open JonRowe opened this issue 2 years ago • 8 comments

:wave: Thanks for all the hard work on Absinthe, I think I've found a bug in open ended scalars, we're using them to transport values where we don't own the structure for from a third party to our UI, I actually tried to type these but unions don't support all the types we would need and for queries the open ended scalar works fine, the problem happens trying to use this scalar for mutations, one of the sub type data we need to send is a list of booleans which causes Absinthe to decide the argument is invalid (because its a list).

I can appreciate you might decide this is not a bug, because why is a scalar sending down a list, but given that its valid json to have a root list and given that the inverse works its at least inconsistent if not a bug.

Environment

  • Elixir version (elixir -v):
  • Absinthe version (mix deps | grep absinthe): 1.7.6
  • Client Framework and version (Relay, Apollo, etc): N/A

Expected behavior

Absinthe sees the field is an open ended scalar and passes the parsed JSON result through as is.

Actual behavior

Absinthe sees a list and decides its invalid.

Relevant Schema/Middleware Code

scalar :magic, open_ended: true do
  parse(& {:ok, &1})
  serialize(& &1)
end

input_object :something do
  field :value, :magic
end

JonRowe avatar Feb 01 '24 15:02 JonRowe

Hi @JonRowe can you show a concrete example?

benwilson512 avatar Feb 01 '24 15:02 benwilson512

defmodule MagicValuesTest do
  use ExUnit.Case, async: true

  defmodule Schema do
    use Absinthe.Schema

    scalar :magic, open_ended: true do
      parse(&{:ok, &1})
      serialize(& &1)
    end

    object :something do
      field :name, non_null(:string)
      field :value, :magic
    end

    input_object :something_input do
      field :name, non_null(:string)
      field :value, :magic
    end

    query do
      field :get_values, non_null(list_of(non_null(:something))) do
        resolve(fn _, _, _ ->
          {:ok, [%{name: "thing_one", value: ["magic"]}]}
        end)
      end
    end

    mutation do
      field :patch_values, type: non_null(list_of(non_null(:something))) do
        arg(:somethings, non_null(list_of(non_null(:something_input))))

        resolve(fn _, %{somethings: somethings}, _ -> {:ok, somethings} end)
      end
    end
  end

  setup do
    Absinthe.Test.prime(Schema)
  end

  test "query passes" do
    assert {:ok, %{data: %{"getValues" => [%{"name" => "thing_one", "value" => ["magic"]}]}}} =
             """
             query {
               getValues {
                 name
                 value
               }
             }
             """
             |> Absinthe.run(Schema, context: %{}, variables: %{})
  end

  test "mutation passes without a list" do
    assert {:ok, %{data: %{"patchValues" => [%{"name" => "thing_one", "value" => _}]}}} =
             """
             mutation patchValues($somethings: [SomethingInput!]!) {
               patchValues(somethings: $somethings) {
                 name
                 value
               }
             }
             """
             |> Absinthe.run(
               Schema,
               context: %{},
               variables: %{
                 "somethings" => [
                   %{"name" => "thing_one", "value" => "magic"}
                 ]
               }
             )
  end

  test "mutation fails" do
    assert {:ok, %{data: %{"patchValues" => [%{"name" => "thing_one", "value" => ["magic"]}]}}} =
             """
             mutation patchValues($somethings: [SomethingInput!]!) {
               patchValues(somethings: $somethings) {
                 name
                 value
               }
             }
             """
             |> Absinthe.run(
               Schema,
               context: %{},
               variables: %{
                 "somethings" => [
                   %{"name" => "thing_one", "value" => ["magic"]}
                 ]
               }
             )
  end
end

JonRowe avatar Feb 01 '24 15:02 JonRowe

Hey Jon I think you're confused abut how open ended scalars are supposed to work. You've got your open ended scalar deep inside other structures. Those structures are not invalidated or consumed by the scalar just because they have an open ended scalar inside. If you want to have your open ended scalar consume the entire input you'd do something more like:

arg :somethings, :magic

I can try to find time later to edit your example to show what I mean. The reason you have versions that pass is that you are passing in JSON structures that are just straight up compatible with the values you have. It'd work even if you used :string instead of :magic.

benwilson512 avatar Feb 01 '24 15:02 benwilson512

Those structures are not invalidated or consumed by the scalar just because they have an open ended scalar inside.

I know that, thats the point, I want name to be a mandatory string, and I only want value to be open ended, hence why I showed if I use another non list value it works, objects, strings, booleans all work in the fashion I expect its only the list that triggers the invalid argument

JonRowe avatar Feb 01 '24 15:02 JonRowe

Apologies, I misread the test. Let me dig into this later today.

benwilson512 avatar Feb 01 '24 16:02 benwilson512

the problem happens trying to use this scalar for mutations, one of the sub type data we need to send is a list

I'm facing the same requirements right now, and I was hoping that open-ended scalars would help here so I could avoid needing to have a JSON string scalar. Any advice if that could be accomplished?

Wigny avatar May 08 '24 21:05 Wigny

As a workaround you can use a structure like {entries: list} as the scalar, its not great but it works

JonRowe avatar May 09 '24 07:05 JonRowe