Request for comments: extensibilty
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 💜