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

Request for comments: extensibilty #30

Open
cabello opened this issue Oct 4, 2019 · 0 comments
Open

Request for comments: extensibilty #30

cabello opened this issue Oct 4, 2019 · 0 comments

Comments

@cabello
Copy link

cabello commented Oct 4, 2019

Hi 👋

We are working on building a federated graph and this gem is helping us getting closer to a production release. 😄 I've been tasked to control the visibility of the schema so the external Federated schema does not expose every single field from internal schemas.

So far my proposed solution is simple, users opt-in to have their fields/objects exposed, I am following the documentation on Limiting Visibility and Extending the GraphQL-Ruby Type Definition System and this is what I come up with:

class BaseField < GraphQL::Schema::Field
  include ApolloFederation::Field

  argument_class BaseArgument

  def initialize(*_args, expose: false, **_kwargs)
    @exposed = exposed
    super(*args, **kwargs, &block)
  end

  def to_graphql
    field_definition = super
    field_definition.metadata[:expose] = @expose
    field_definition
  end
end

# ---

class Query < BaseObject
  field :foobar, Foobar, null: true, expose: true
end

# ---

# controller
result = GraphSchema.execute(query, variables: variables, operation_name: operation_name, context: context, only: ExposeWhitelist.new)

# ---

# Work in progress

class ExposeWhitelist
  def call(schema_member, _context)
    if schema_member.is_a?(GraphQL::Field)
      # TODO: better detection of Federated fields.
      schema_member.name == "_entities" ||
        schema_member.name == "_service" ||
        schema_member.name == "sdl" ||
        schema_member.introspection? ||
        schema_member.metadata[:exposed]
    elsif schema_member.is_a?(GraphQL::Argument)
      # TODO: implement logic here to decide either or not Argument should be exposed.
      true
    else
      # TODO: implement logic here to decide either or not Type, Enum should be exposed.
      true
    end
  end
end

This causes the following error:

{
  "errors": [
    {
      "message": "A copy of Types has been removed from the module tree but is still active!",
      "extensions": {
        "type": "ArgumentError",

It seems that the challenge lies around the fact that the module ApolloFederation::Field also defines an initialize method and the super calls gets confused 🤔 and crashes in unexpected ways, the bare minimum example I tried just overwrote initialize while including the Federation module and that was enough to cause issues.

Based of the following technique for extending a method from a module, I came up with the following solution:

module ApolloFederation
  module Field
    include HasDirectives

    def self.included(base)
      base.extend ClassMethods
      base.overwrite_initialize
      base.instance_eval do
        def method_added(name)
          return if name != :initialize
          overwrite_initialize
        end
      end
    end

    module ClassMethods
      def overwrite_initialize
        class_eval do
          unless method_defined?(:apollo_federation_initialize)
            define_method(:apollo_federation_initialize) do |*args, external: false, requires: nil, provides: nil, **kwargs, &block|
              if external
                add_directive(name: 'external')
              end
              if requires
                add_directive(
                  name: 'requires',
                  arguments: [
                    name: 'fields',
                    values: requires[:fields],
                  ],
                )
              end
              if provides
                add_directive(
                  name: 'provides',
                  arguments: [
                    name: 'fields',
                    values: provides[:fields],
                  ],
                )
              end
              original_initialize(*args, **kwargs, &block)
            end
          end

          if instance_method(:initialize) != instance_method(:apollo_federation_initialize)
            alias_method :original_initialize, :initialize
            alias_method :initialize, :apollo_federation_initialize)
          end
        end
      end
    end
  end
end

Thoughts on this approach, would you consider a Pull Request with similar change?

Thanks for open sourcing this Apollo Federation solution, I appreciate it 💜

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

No branches or pull requests

2 participants