Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Absinthe.Relay.Node.ParseIDs causes runtime error if occurs more than once in a mutation query #960

Closed
jacobparry opened this issue Jul 23, 2020 · 3 comments

Comments

@jacobparry
Copy link

jacobparry commented Jul 23, 2020

If submitting a bug, please provide the following:

Environment

  • Elixir version (elixir -v):
    elixir 1.10.2
    erlang 22.3.4.1

  • Absinthe version (mix deps | grep absinthe):
    absinthe (Hex package) (mix) locked at 1.5.2 (absinthe) 669c8487
    absinthe_relay (Hex package) (mix) locked at 1.5.0 (absinthe_relay) 173cc44c
    absinthe_sorting_codec 1.0.0 (Hex package) (mix) locked at 1.0.0 (absinthe_sorting_codec) e5dd8152
    absinthe_sdl 1.1.0 (Hex package) (mix) locked at 1.1.0 (absinthe_sdl) 8b5f6293
    absinthe_plug (Hex package) (mix) locked at 1.5.0 (absinthe_plug) 4c160f4c
    absinthe_phoenix (Hex package) (mix) locked at 1.5.0 (absinthe_phoenix) bbe443e8

  • Client Framework and version (Relay, Apollo, etc):
    None, this is failing from unit tests

Expected behavior

In the ParseIDs middleware, there is a function find_schema_root!/2. I have several inspects in place to see what is going on (which I will past below). I expect to see the following happen (This works correctly on Absinthe 1.4 but not on Absinthe 1.5)

"*********************************************"
field_dentifier: :add_tag_value_to_transaction
map_get_parse_ids_root: :input
field_args: %{
  input: %Absinthe.Type.Argument{
    __reference__: %{
      identifier: :input,
      location: %{
        file: "/Users/jacobparry/Divvy/repos/juno/apps/shared_api/lib/schema/mutations/transaction.ex",
        line: 0
      },
      module: SharedAPI.Schema.Mutations.Transaction
    },
    default_value: nil,
    deprecation: nil,
    description: nil,
    name: "input",
    type: %Absinthe.Type.NonNull{of_type: :add_tag_value_to_transaction_input}
  }
}
root_argument: :input
map_get_root_argument: %Absinthe.Type.Argument{
  __reference__: %{
    identifier: :input,
    location: %{
      file: "/Users/jacobparry/Divvy/repos/juno/apps/shared_api/lib/schema/mutations/transaction.ex",
      line: 0
    },
    module: SharedAPI.Schema.Mutations.Transaction
  },
  default_value: nil,
  deprecation: nil,
  description: nil,
  name: "input",
  type: %Absinthe.Type.NonNull{of_type: :add_tag_value_to_transaction_input}
}
"*********************************************"
field_identifier: :tag_values
map_get_parse_ids_root: nil

I expect the parse_ids_root to be nil for the child fields that get resolved in the mutation.

mutation_before_parse: %{
  client_mutation_id: "0",
  tag_value_id: "VGFnVmFsdWU6MTAwMQ==",
  transaction_id: "VHJhbnNhY3Rpb246Y2Q4MTY4ZjYtZmM3Zi00NjUwLTgwMGEtZGM5Zjg1YTQ2NWIw"
}
"*********************************************"
field_dentifier: :add_tag_value_to_transaction
map_get_parse_ids_root: :input
field_args: %{
  input: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :input,
    name: "input",
    type: %Absinthe.Type.NonNull{of_type: :add_tag_value_to_transaction_input}
  }
}
root_argument: :input
map_get_root_argument: %Absinthe.Type.Argument{
  __reference__: nil,
  default_value: nil,
  definition: nil,
  deprecation: nil,
  description: nil,
  identifier: :input,
  name: "input",
  type: %Absinthe.Type.NonNull{of_type: :add_tag_value_to_transaction_input}
}
mutation_after_parse: %{
  client_mutation_id: "0",
  tag_value_id: 1001,
  transaction_id: "cd8168f6-fc7f-4650-800a-dc9f85a465b0"
}
tags_before_parse: %{first: 10}
"*********************************************"
field_dentifier: :tag_values
map_get_parse_ids_root: :input
field_args: %{
  after: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :after,
    name: "after",
    type: :string
  },
  before: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :before,
    name: "before",
    type: :string
  },
  first: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :first,
    name: "first",
    type: :integer
  },
  last: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :last,
    name: "last",
    type: :integer
  },
  tag_type_id: %Absinthe.Type.Argument{
    __reference__: nil,
    default_value: nil,
    definition: nil,
    deprecation: nil,
    description: nil,
    identifier: :tag_type_id,
    name: "tag_type_id",
    type: :id
  }
}
root_argument: :input
map_get_root_argument: nil

In this result (Absinthe 1.5), we see that when the ParseIDs middleware tries to parse ids for the child field, it finds the parse_ids_root is still set as :input from the mutation, when it shouldn't. This behavior differs from Absinth 1.4.

Basically, the first ParseIDs works great parsing the IDs from the input object generated by the mutation. However, once the graph tries to parse the child field that also contains a ParseIDs middleware call, it blows up.

This results in a runtime error:

** (RuntimeError) Can't find ParseIDs schema root argument :input
     stacktrace:
       (shared_api 0.1.0) lib/absinthe/parse_ids.ex:259: Absinthe.SharedAPI.Absinthe.ParseIDs.find_schema_root!/2
       (shared_api 0.1.0) lib/absinthe/parse_ids.ex:224: Absinthe.SharedAPI.Absinthe.ParseIDs.parse/3
       (shared_api 0.1.0) lib/absinthe/parse_ids.ex:206: Absinthe.SharedAPI.Absinthe.ParseIDs.call/2
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:230: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:185: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/3
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:170: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:280: Absinthe.Phase.Document.Execution.Resolution.build_result/3
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:170: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:280: Absinthe.Phase.Document.Execution.Resolution.build_result/3
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:170: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:67: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3
       (absinthe 1.5.2) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3
       (absinthe 1.5.2) lib/absinthe/pipeline.ex:368: Absinthe.Pipeline.run_phase/3
       (absinthe_plug 1.5.0) lib/absinthe/plug.ex:445: Absinthe.Plug.run_query/4
       (absinthe_plug 1.5.0) lib/absinthe/plug.ex:258: Absinthe.Plug.call/2
       (phoenix 1.4.17) lib/phoenix/router.ex:288: Phoenix.Router.__call__/2
       (user_api 0.1.0) lib/user_api/endpoint.ex:1: UserAPI.Endpoint.plug_builder_call/2 

Relevant Schema/Middleware Code

Mutation being run. There is a ParseIDs middleware call on both :add_tag_value_to_transaction and on :tag_values.

        mutation TestAddTagValueToTransaction($input: AddTagValueToTransactionInput!) {
          addTagValueToTransaction(input: $input) {
            transaction {
              id
              status
              tagValues(first: 10) {
                edges {
                  node {
                    id
                  }
                }
              }
            }
          }
        }
      """

Mutation schema (notice the ParseIDs middleware):

 payload field(:add_tag_value_to_transaction) do
      input do
        field(:transaction_id, non_null(:id))
        field(:tag_value_id, :id, deprecate: "Use tagValueIds")
        field(:tag_value_ids, list_of(:id))
      end

      output do
        field(:transaction, :transaction)
      end

      middleware(fn res, _ ->
        res.arguments |> IO.inspect(label: :mutation_before_parse)
        res
      end)

      middleware(ParseIDs, transaction_id: :transaction, tag_value_id: :tag_value, tag_value_ids: :tag_value)

      middleware(fn res, _ ->
        res.arguments |> IO.inspect(label: :mutation_after_parse)
        res
      end)

      resolve(&TagValueResolver.add_to_transaction/2)
    end

Transaction node type (notice that this :tag_values field is part of the mutation query):
The mutation returns a transaction which then requests the child tag_values field.

node object :transaction do
   ...
    connection field(:tag_values, node_type: :tag_value, deprecate: "Use `tags` instead") do
      arg(:tag_type_id, :id)

      middleware(fn res, _ ->
        res.arguments |> IO.inspect(label: :tags_before_parse)
        res
      end)

      middleware(ParseIDs, tag_type_id: :tag_type)

      middleware(fn res, _ ->
        res.arguments |> IO.inspect(label: :tags_after_parse)
        res
      end)

      resolve(&TransactionRecordResolver.tag_values/2)
    end
...
end

find_schema_root!/2 function with inspects so you can see what the output above is looking at:

  @spec find_schema_root!(Field.t(), Resolution.t()) ::
          {{Field.t() | Argument.t(), String.t()}, (String.t() -> String.t())}
  defp find_schema_root!(field, resolution) do
    IO.inspect("*********************************************")
    IO.inspect(field.identifier, label: :field_dentifier)

    Map.get(resolution.private, :__parse_ids_root)
    |> IO.inspect(label: :map_get_parse_ids_root)

    case Map.get(resolution.private, :__parse_ids_root) do
      nil ->
        {field, & &1}

      root_argument ->
        IO.inspect(field.args, label: :field_args)
        IO.inspect(root_argument, label: :root_argument)

        Map.get(field.args, root_argument)
        |> IO.inspect(label: :map_get_root_argument)

        argument =
          Map.get(field.args, root_argument) ||
            raise "Can't find ParseIDs schema root argument #{inspect(root_argument)}"

        field_error_prefix = error_prefix(field, resolution.adapter)
        argument_error_prefix = error_prefix(argument, resolution.adapter)

        {argument,
         &String.replace_leading(
           &1,
           field_error_prefix,
           field_error_prefix <> argument_error_prefix
         )}
    end
  end
@benwilson512
Copy link
Contributor

Got it. So when calling ParseIds on the connection field it's expecting things to be nested under input as if it were a payload field. Looking into it.

@jacobparry
Copy link
Author

@benwilson512 This seems to only happen from mutation payload fields. Regular queries don't appear to be affected.

@jacobparry
Copy link
Author

jacobparry commented Jul 27, 2020

@benwilson512 It looks like this bug has been identified to be an issue here: absinthe-graphql/absinthe_relay#150

Absinthe Relay is the correct place for this bug, so I'm going to close this issue and open the issue in the absinthe relay repo. absinthe-graphql/absinthe_relay#162

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants