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

GraphQL::Schema::FieldExtension should not modify options #4733

Closed
gael-ian opened this issue Dec 18, 2023 · 2 comments
Closed

GraphQL::Schema::FieldExtension should not modify options #4733

gael-ian opened this issue Dec 18, 2023 · 2 comments

Comments

@gael-ian
Copy link

gael-ian commented Dec 18, 2023

Describe the bug

We need to apply the same extension options to several fields. To reduce duplication, we'd like to use the .with_options method provided by ActiveSupport on any Object but it does not work as expected.

Steps to reproduce

Imagine we have something like that:

# May come from an external gem
class Extension < ::GraphQL::Schema::FieldExtension
  def apply = @name = options.delete :name
  def resolve(context:, object:, arguments:, **_rest) = yield object, arguments, name:
end

class Field < ::GraphQL::Schema::Field
  def initialize(...)
    super
    extension(Extension)
  end
end

class Mutation < Types::Objects::Base
  field_class Field

  with_options extension_options: { name: :value } do
    field :record_create, mutation: Records::Create
    field :record_update, mutation: Records::Update
  end
end

In GraphQL::Schema::FieldExtension, the constructor simply assign options to the extension. If the extension manipulate its options Hash (with #merge or #delete), it will also be altered for subsequent calls. In case of a #delete, later fields will not be extended.

Expected behavior

with_options should work as expected and actually forward options to every .field call.

Actual behavior

with_options works as expected but field extensions may consume options in a destructive way.

This can be fixed with a single line change as:

module GraphQL
  class Schema
    class FieldExtension
      # […]
      def initialize(field:, options:)
        @field = field
-       @options = options || {}
+       @options = options&.dup || {}
        @added_default_arguments = nil
        apply
      end
      # […]
    end
  end
end
@rmosolgo
Copy link
Owner

Hi!

I don't want to add a .dup to the default behavior because most of the time, it will be a needless allocation. I'd rather keep the default path as efficient as I can.

Instead, you might:

  • Implement def initialize to copy the incoming options, as you've suggested above;
  • Or, modify def apply not to modify options, eg @name = options[:name] instead
  • Or, pass fresh options to each extensions ... call instead of reusing the same Hash.

I hope one of those options works for you!

@gael-ian
Copy link
Author

Hi @rmosolgo

And thank you for your answer.

Sadly, the first two solutions can't be applied to field extensions provided by an external gem (without monky-patching them) but I understand your point.

Thank you

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