rmosolgo/graphql-ruby

Load order for interfaces implementing interfaces

jacquesn opened this issue · 6 comments

Describe the bug

If Obect A implements Interface A, which implements interface B, which has a field that returns Object A, Object A will have the field but not the interface.

Versions

graphql version: 2.3.14
rails (or other framework): 7.1.3.4

GraphQL schema

module Types
  class ObjectA < Types::BaseObject
    implements InterfaceA
  end
end

module Types
  module InterfaceA
    include Types::BaseInterface

    implements InterfaceB
  end
end

module Types
  module InterfaceB
    include Types::BaseInterface

    field :my_field, ObjectA, null: false
  end
end

Steps to reproduce

Make a new Rails app with the graphql gem and add the above types.

Expected behavior

Types::ObjectA.interfaces # => [Types::InterfaceB, Types::InterfaceA]

Actual behavior

Types::ObjectA.interfaces # => [Types::InterfaceA]

Additional context

Interestingly, even though ObjectA doesn't have Types::InterfaceB in its interfaces, it does have myField in its fields:

Types::ObjectA.fields.pluck(0) # => ["myField"]

Since the field is still there, it's perhaps not a huge problem, but it does break things when used with graphiql, resulting in an error that looks like this:

Type ObjectA must implement InterfaceB because it is implemented by InterfaceA.

so that graphiql does not work.

This error only happens because my_field returns ObjectA. If you change ObjectA to String, it works as expected.

Hey, thanks for the detailed write up ... I wonder how this can be made to work nicely with Rails.

For work-arounds, we have a couple of options:

  • Use a string for ObjectA in the field definition:

    field :my_field, "Types::ObjectA", null: false 
  • Move type(ObjectA) in to a do ... end block for field :my_field:

    field :my_field, null: false do 
      type(ObjectA)
    end 

I think one or both of those will let GraphQL-Ruby defer its reference to ObjectA, allowing Rails's autoloader to do its job better. Could you give those a try and let me know what you find?

Thanks for the quick response!

Oh cool—I didn't know either of those were options.

The string option works.

The block option seems to work at first, setting the interfaces correctly, but

Types::ObjectA.fields
# => undefined method `field' for an instance of Types::BaseField

I'll be going with the string option. That works nicely for me, and I'm glad to have a quick solution that didn't even need a change to the gem!

I'm glad that first one works for you. I'd really like to fix that error you encountered when moving the call to the block. Would you mind sharing the full backtrace for it?

Sure:

["/app/graphql/types/interface_b.rb:15:in `block in <module:InterfaceB>'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/field.rb:366:in `instance_eval'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/field.rb:366:in `ensure_loaded'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/member/has_fields.rb:164:in `block (2 levels) in fields'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/member/has_fields.rb:160:in `each'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/member/has_fields.rb:160:in `block in fields'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/member/has_fields.rb:158:in `each'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/graphql-2.3.14/lib/graphql/schema/member/has_fields.rb:158:in `fields'",
 "(irb):1:in `<main>'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb/workspace.rb:121:in `eval'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb/workspace.rb:121:in `evaluate'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14
.0/lib/irb/context.rb:633:in `evaluate_expression'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb/context.rb:601:in `evaluate'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1049:in `block (2 levels) in eval_input'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1388:in `signal_status'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1041:in `block in eval_input'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1120:in `block in each_top_level_statement'",
 "<internal:kernel>:187:in `loop'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1117:in `each_top_level_statement'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1040:in `eval_input'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1021:in `block in run'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1020:in `catch'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:1020:in `run'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/irb-1.14.0/lib/irb.rb:904:in `start'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/commands/console/console_command.rb:78:in `start'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/commands/console/console_command.rb:16:in `start'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/commands/console/console_command.rb:106:in `perform'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor/command.rb:28:in `run'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor/invocation.rb:127:in `invoke_command'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/command/base.rb:178:in `invoke_command'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/thor-1.3.1/lib/thor.rb:527:in `dispatch'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/command/base.rb:73:in `perform'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/command.rb:71:in `block in invoke'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/command.rb:149:in `with_argv'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/command.rb:69:in `invoke'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/railties-7.1.3.4/lib/rails/commands.rb:18:in `<main>'",
 "/.rbenv/versions/3.3.1/lib/ruby/3.3.0/bundled_gems.rb:74:in `require'",
 "/.rbenv/versions/3.3.1/lib/ruby/3.3.0/bundled_gems.rb:74:in `block (2 levels) in replace_require'",
 "/.rbenv/versions/3.3.1/lib/ruby/gems/3.3.0/gems/bootsnap-1.18.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'",
 "bin/rails:4:in `<main>'"]

Derp, my bad. It's a typo in my original suggestion. It should be type(...) inside the block, not field(...). I'll update the comment above accordingly:

- field(ObjectA)
+ type(ObjectA)

Thanks for sharing that, and please let me know if you run into any other trouble!

Oh—haha. Yes, that works for me now.