Code Monkey home page Code Monkey logo

apollo-federation-ruby's Introduction

apollo-federation

CircleCI

This gem extends the GraphQL Ruby gem to add support for creating an Apollo Federation schema.

Installation

Add this line to your application's Gemfile:

gem 'apollo-federation'

And then execute:

$ bundle

Or install it yourself as:

$ gem install apollo-federation

Getting Started

Include the ApolloFederation::Argument module in your base argument class:

class BaseArgument < GraphQL::Schema::Argument
  include ApolloFederation::Argument
end

Include the ApolloFederation::Field module in your base field class:

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

  argument_class BaseArgument
end

Include the ApolloFederation::Object module in your base object class:

class BaseObject < GraphQL::Schema::Object
  include ApolloFederation::Object

  field_class BaseField
end

Include the ApolloFederation::Interface module in your base interface module:

module BaseInterface
  include GraphQL::Schema::Interface
  include ApolloFederation::Interface

  field_class BaseField
end

Include the ApolloFederation::Union module in your base union class:

class BaseUnion < GraphQL::Schema::Union
  include ApolloFederation::Union
end

Include the ApolloFederation::EnumValue module in your base enum value class:

class BaseEnumValue < GraphQL::Schema::EnumValue
  include ApolloFederation::EnumValue
end

Include the ApolloFederation::Enum module in your base enum class:

class BaseEnum < GraphQL::Schema::Enum
  include ApolloFederation::Enum

  enum_value_class BaseEnumValue
end

Include the ApolloFederation::InputObject module in your base input object class:

class BaseInputObject < GraphQL::Schema::InputObject
  include ApolloFederation::InputObject

  argument_class BaseArgument
end

Include the ApolloFederation::Scalar module in your base scalar class:

class BaseScalar < GraphQL::Schema::Scalar
  include ApolloFederation::Scalar
end

Finally, include the ApolloFederation::Schema module in your schema:

class MySchema < GraphQL::Schema
  include ApolloFederation::Schema
end

Optional: To opt in to Federation v2, specify the version in your schema:

class MySchema < GraphQL::Schema
  include ApolloFederation::Schema
  federation version: '2.0'
end

Example

The example folder contains a Ruby implementation of Apollo's federation-demo. To run it locally, install the Ruby dependencies:

$ bundle

Install the Node dependencies:

$ yarn

Start all of the services:

$ yarn start-services

Start the gateway:

$ yarn start-gateway

This will start up the gateway and serve it at http://localhost:5000.

Usage

The API is designed to mimic the API of Apollo's federation library. It's best to read and understand the way federation works, in general, before attempting to use this library.

Extending a type

Apollo documentation

Call extend_type within your class definition:

class User < BaseObject
  extend_type
end

The @key directive

Apollo documentation

Call key within your class definition:

class User < BaseObject
  key fields: :id
end

Compound keys are also supported:

class User < BaseObject
  key fields: [:id, { organization: :id }]
end

As well as non-resolvable keys:

class User < BaseObject
  key fields: :id, resolvable: false
end

See field set syntax for more details on the format of the fields option.

The @external directive

Apollo documentation

Pass the external: true option to your field definition:

class User < BaseObject
  field :id, ID, null: false, external: true
end

The @requires directive

Apollo documentation

Pass the requires: option to your field definition:

class Product < BaseObject
  field :price, Int, null: true, external: true
  field :weight, Int, null: true, external: true
  field :shipping_estimate, Int, null: true, requires: { fields: [:price, :weight] }
end

See field set syntax for more details on the format of the fields option.

The @provides directive

Apollo documentation

Pass the provides: option to your field definition:

class Review < BaseObject
  field :author, 'User', null: true, provides: { fields: :username }
end

See field set syntax for more details on the format of the fields option.

The @shareable directive (Apollo Federation v2)

Apollo documentation

Call shareable within your class definition:

class User < BaseObject
  shareable
end

Pass the shareable: true option to your field definition:

class User < BaseObject
  field :id, ID, null: false, shareable: true
end

The @inaccessible directive (Apollo Federation v2)

Apollo documentation

Call inaccessible within your class definition:

class User < BaseObject
  inaccessible
end

Pass the inaccessible: true option to your field definition:

class User < BaseObject
  field :id, ID, null: false, inaccessible: true
end

The @override directive (Apollo Federation v2)

Apollo documentation

Pass the override: option to your field definition:

class Product < BaseObject
  field :id, ID, null: false
  field :inStock, Boolean, null: false, override: { from: 'Products' }
end

The @tag directive (Apollo Federation v2)

Apollo documentation

Call tag within your class definition:

class User < BaseObject
  tag name: 'private'
end

Pass the tags: option to your field definition:

class User < BaseObject
  field :id, ID, null: false, tags: [{ name: 'private' }]
end

Field set syntax

Field sets can be either strings encoded with the Apollo Field Set syntax or arrays, hashes and snake case symbols that follow the graphql-ruby conventions:

# Equivalent to the "organizationId" field set
:organization_id

# Equivalent to the "price weight" field set
[:price, :weight]

# Equivalent to the "id organization { id }" field set
[:id, { organization: :id }]

Reference resolvers

Apollo documentation

Define a resolve_reference class method on your object. The method will be passed the reference from another service and the context for the query.

class User < BaseObject
  key fields: :user_id
  field :user_id, ID, null: false
  
  def self.resolve_reference(reference, context)
    USERS.find { |user| user[:userId] == reference[:userId] }
  end
end

To maintain backwards compatibility, by default, reference hash keys are camelcase. They can be underscored by setting underscore_reference_keys on your entity class. In order to maintain consistency with GraphQL Ruby, we may change the keys to be underscored by default in a future major release.

class User < BaseObject
  key fields: :user_id
  field :user_id, ID, null: false
  underscore_reference_keys true
  
  def self.resolve_reference(reference, context)
    USERS.find { |user| user[:user_id] == reference[:user_id] }
  end
end

Alternatively you can change the default for your project by setting underscore_reference_keys on BaseObject:

class BaseObject < GraphQL::Schema::Object
  include ApolloFederation::Object

  field_class BaseField
  underscore_reference_keys true
end

Tracing

To support federated tracing:

  1. Add use ApolloFederation::Tracing to your schema class.
  2. Change your controller to add tracing_enabled: true to the execution context based on the presence of the "include trace" header:
    def execute
      # ...
      context = {
        # Pass in the headers from your web framework. For Rails this will be request.headers
        # but for other frameworks you can pass the Rack env.
        tracing_enabled: ApolloFederation::Tracing.should_add_traces(request.headers)
      }
      # ...
    end

Exporting the Federated SDL

When using tools like rover for schema validation, etc., add a Rake task that prints the Federated SDL to a file:

namespace :graphql do
  namespace :federation do
    task :dump do
      File.write("schema.graphql", MySchema.federation_sdl)
    end
  end
end

Example validation check with Rover and Apollo Studio:

bin/rake graphql:federation:dump
rover subgraph check mygraph@current --name mysubgraph --schema schema.graphql

Testing the federated schema

This library does not include any testing helpers currently. A federated service receives subgraph queries from the Apollo Gateway via the _entities field and that can be tested in a request spec.

With Apollo Gateway setup to hit your service locally or by using existing query logs, you can retrieve the generated _entities queries.

For example, if you have a blog service that exposes posts by a given author, the query received by the service might look like this.

query($representations: [_Any!]!) {
  _entities(representations: $representations) {
    ... on BlogPost {
      id
      title
      body
    }
  }
}

Where $representations is an array of entity references from the gateway.

{
  "representations": [
    {
      "__typename": "BlogPost",
      "id": 1
    },
    {
      "__typename": "BlogPost",
      "id": 2
    }
  ]
}

Using RSpec as an example, a request spec for this query.

it "resolves the blog post entities" do
  blog_post = BlogPost.create!(attributes)

  query = <<~GRAPHQL
    query($representations: [_Any!]!) {
      _entities(representations: $representations) {
        ... on BlogPost {
          id
          title
          body
        }
      }
    }
  GRAPHQL

  variables = { representations: [{ __typename: "BlogPost", id: blog_post.id }] }

  result = Schema.execute(query, variables: variables)

  expect(result.dig("data", "_entities", 0, "id")).to eq(blog_post.id)
end

See discussion at #74 and an internal spec that resolves _entities for more details.

Known Issues and Limitations

  • For GraphQL older than 1.12, the interpreter runtime has to be used.
  • Does not add directives to the output of Schema.to_definition. Since graphql-ruby doesn't natively support schema directives, the directives will only be visible to the Apollo Gateway through the Query._service field (see the Apollo Federation specification) or via Schema#federation_sdl as explained above.

Maintainers

Gusto GraphQL Team:

apollo-federation-ruby's People

Contributors

ashleyyy avatar bilby91 avatar cmschuetz avatar col avatar daemonsy avatar dependabot[bot] avatar dweinand avatar erikkessler1 avatar ethankhall avatar geshwho avatar grxy avatar hectormf avatar jmpage avatar jturkel avatar konalegi avatar kryptonat avatar lennyburdette avatar matthewjf avatar moonflare avatar prikha avatar rtymchyk avatar rylanc avatar semantic-release-bot avatar slauppy avatar vecerek avatar voidfiles avatar zachahn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apollo-federation-ruby's Issues

Making `Query._entities` work with GraphQL Dataloader

Background

GraphQL Ruby has its own native batch loading abstraction that uses Ruby's fibers.

Right now, we are using it to resolve all N+1 queries in our app, except for incoming top level subgraph queries which are resolved by the Query._entities field.

The problem

Given a query:

query($representations:[_Any!]!){
  _entities(representations:$representations){
    ...on BlogPost{ 
      id
      title
    }
  }
}

Where we have variables representing two objects to be resolved:

{
  "variables": {
    "representations": [
      {
        "__typename": "BlogPost",
        "id": "1"
      },
      {
        "__typename": "BlogPost",
        "id": "2"
      }
    ]
  }
}

One might write a simple BlogPostType.resolve_reference method that looks like this:

def self.resolve_reference(reference, context)
  
  post = context.dataloader.with(DataSources::ActiveRecord, BlogPost).load(reference[:id])
  
  return nil unless post

  BlogPostService.new(blog_post: post)
end

However, this doesn't work with Dataloader. With debugging statements around it, The moment #load is called, we also end up instantiating theDataSource and call #fetch on it. This ends up generating the N+1s as before.

Expected behavior

In Apollo's own implementation, the typical recommendation is to do batch loading in __resolveReference. We can differ in implementation on Ruby, but if we're sticking to the same paradigm, I would hope that there is a way for #resolve_reference to work with the native batch loader.

Possible causes and fixes

Still investigating, will update this section of the issue once I have more information.

As a starting point, this is the resolver for Query._entities. It uses a map of calls to each type's resolve_reference, which probably does not work with the Dataloader.

def _entities(representations:)
representations.map do |reference|
typename = reference[:__typename]
# TODO: Use warden or schema?
type = context.warden.get_type(typename)
if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
# TODO: Raise a specific error class?
raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \
' but no object type of that name was found in the schema'
end
# TODO: What if the type is an interface?
type_class = type.is_a?(GraphQL::ObjectType) ? type.metadata[:type_class] : type
if type_class.respond_to?(:resolve_reference)
result = type_class.resolve_reference(reference, context)
else
result = reference
end
context.schema.after_lazy(result) do |resolved_value|
# TODO: This isn't 100% correct: if (for some reason) 2 different resolve_reference calls
# return the same object, it might not have the right type
# Right now, apollo-federation just adds a __typename property to the result,
# but I don't really like the idea of modifying the resolved object
context[resolved_value] = type
resolved_value
end

[question] Is this project maintained?

Firstly, thank so much for the gem!

We consider to use this gem, but the last update is 2020/10 and seem there are no activities since that time. Is there a plan to continue maintain this gem?

How to test/confirm federation implementation?

Thanks for this gem, I think it will do just what I need. I've added the gem to my app and followed the steps for updating my schema and classes. Is there any documentation in regard to how I should test to confirm my changes?

Passing data in context will cause a crash

Given a schema call with context
result = schema.execute(
query,
operation_name: operation_name,
variables: vars,
context: { current_user: 'Bob' }
)

{:query=>"query GetServiceDefinition { _service { sdl } }", :operationName=>nil, :vars=>{}, :schema=>ProductSchema}

will result in

ERROR ArgumentError: unknown keyword: current_user

Given a token we include the current user in all contexts. I would not expect this to be an issue however.

Error in loading schema for query

include ApolloFederation::Schema
...
mutation(Types::Mutation)
query(Types::Query)
...
end

Without the query() things work fine.
Once I add the query() i get the below error

TypeError: Cannot read property 'types' of undefined
    at buildClientSchema 
(http://localhost:3001/assets/graphiql/rails/application-6f03c7a92432765b0fbe671bfb7a292ee6c37e6704b6e6ac95587e45676bbf72.js:34102:72)
    at http://localhost:3001/assets/graphiql/rails/application-6f03c7a92432765b0fbe671bfb7a292ee6c37e6704b6e6ac95587e45676bbf72.js:2795:55

Please advise

P.S.: I'm very new to graphql. Might be missing out on something

Consistent treatment for strings and symbols in field references

#105 introduced new behavior that allows us to specify fields as symbols and automatically have the camelcased (e.g. :product_id becomes productId). In order to keep the change backwards compatible, the behavior of Strings remains unchanged. This means that there is inconsistent behavior between strings and symbols.

It would be great it, instead, we also camelize string input and give a global configuration option to either disable camelcase. This behavior would also be more inline with how graphql-ruby behaves.

Struggling to extend type in nodejs service

I have been trying to get this working for the past day and have hit a blocker. Any help would be appreciated.

I have created a dummy service - customer service (in nodejs) - and another dummy service - schedule service (in ruby). I want to create a one to many relationship between customer and schedule. i.e. a customer can have many schedules.

I have managed to get queries like the following working

{
  schedules {
    id
    time
    customer {
      id
      schedules {
        id
        time
        customer {
          id
        }
      }
    }
  }
}

So I feel like I have setup everything up in my service correctly. When I call things in the gateway things partially work as well. For example

{
  customers {
    id
    name
    # schedules {
    #   id
    #   # time
    # }
  }
  schedules {
    id
    time
    customer {
      id
      name
      schedules {
        id
      }
    }
  }
}

As soon as I add in the commented out bit I get the following error.

{
  "errors": [
    {
      "message": "500: Internal Server Error",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "response": {
          "url": "http://localhost:3000/graphql",
          "status": 500,
          "statusText": "Internal Server Error",
          "body": {
            "error": {
              "message": "no implicit conversion of #<Class:0x00007f81638ce970> into Hash",
              "backtrace": [
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:524:in `block in resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:661:in `block in with_extensions'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:680:in `block (2 levels) in run_extensions_before_resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:683:in `run_extensions_before_resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:680:in `block in run_extensions_before_resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field_extension.rb:50:in `resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:678:in `run_extensions_before_resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:660:in `with_extensions'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/field.rb:504:in `resolve'",
                "/Users/ryanme/src/ap-apollo-integration-example/scheduling-service/app/graphql/types/base_field.rb:13:in `resolve_field'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/member/instrumentation.rb:80:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/member/instrumentation.rb:80:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/field.rb:248:in `resolve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:321:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/middleware_chain.rb:49:in `invoke_core'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema/middleware_chain.rb:38:in `invoke'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:129:in `block in resolve_field'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `block in trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:76:in `call_tracers'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:128:in `resolve_field'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:92:in `block in resolve_selection'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:85:in `each'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:85:in `resolve_selection'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:56:in `block in resolve_root_selection'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `block in trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:76:in `call_tracers'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:49:in `resolve_root_selection'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/execute.rb:31:in `begin_query'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:112:in `begin_query'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:84:in `block in run_as_multiplex'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:83:in `map'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:83:in `run_as_multiplex'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:62:in `block (2 levels) in run_queries'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:186:in `block in instrument_and_analyze'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:29:in `block (2 levels) in apply_instrumenters'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:46:in `block (2 levels) in each_query_call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:41:in `each_query_call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:45:in `block in each_query_call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:44:in `each_query_call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:27:in `block in apply_instrumenters'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/instrumentation.rb:26:in `apply_instrumenters'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:174:in `instrument_and_analyze'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:61:in `block in run_queries'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `block in trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:76:in `call_tracers'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/tracing.rb:62:in `trace'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:59:in `run_queries'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/execution/multiplex.rb:49:in `run_all'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema.rb:392:in `block in multiplex'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema.rb:1279:in `with_definition_error_check'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema.rb:391:in `multiplex'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/graphql-1.9.12/lib/graphql/schema.rb:368:in `execute'",
                "/Users/ryanme/src/ap-apollo-integration-example/scheduling-service/app/controllers/graphql_controller.rb:10:in `execute'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/abstract_controller/base.rb:196:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/rendering.rb:30:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/abstract_controller/callbacks.rb:42:in `block in process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/callbacks.rb:135:in `run_callbacks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/abstract_controller/callbacks.rb:41:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/rescue.rb:22:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/instrumentation.rb:33:in `block in process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/notifications.rb:180:in `block in instrument'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/notifications/instrumenter.rb:24:in `instrument'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/notifications.rb:180:in `instrument'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/instrumentation.rb:32:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal/params_wrapper.rb:245:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/controller_runtime.rb:27:in `process_action'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/abstract_controller/base.rb:136:in `process'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionview-6.0.0/lib/action_view/rendering.rb:39:in `process'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal.rb:191:in `dispatch'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_controller/metal.rb:252:in `dispatch'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/route_set.rb:51:in `dispatch'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/route_set.rb:33:in `serve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/journey/router.rb:49:in `block in serve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/journey/router.rb:32:in `each'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/journey/router.rb:32:in `serve'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/route_set.rb:837:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/tempfile_reaper.rb:15:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/etag.rb:25:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/conditional_get.rb:38:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/head.rb:12:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/http/content_security_policy.rb:18:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/session/abstract/id.rb:232:in `context'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/session/abstract/id.rb:226:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/cookies.rb:648:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activerecord-6.0.0/lib/active_record/migration.rb:567:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/callbacks.rb:101:in `run_callbacks'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/callbacks.rb:26:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/executor.rb:14:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/debug_exceptions.rb:32:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/web-console-4.0.1/lib/web_console/middleware.rb:132:in `call_app'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/web-console-4.0.1/lib/web_console/middleware.rb:28:in `block in call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/web-console-4.0.1/lib/web_console/middleware.rb:17:in `catch'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/web-console-4.0.1/lib/web_console/middleware.rb:17:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/railties-6.0.0/lib/rails/rack/logger.rb:38:in `call_app'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/railties-6.0.0/lib/rails/rack/logger.rb:26:in `block in call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/tagged_logging.rb:80:in `block in tagged'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/tagged_logging.rb:28:in `tagged'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/tagged_logging.rb:80:in `tagged'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/railties-6.0.0/lib/rails/rack/logger.rb:26:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/quiet_assets.rb:13:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/request_id.rb:27:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/method_override.rb:22:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/runtime.rb:22:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/activesupport-6.0.0/lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/executor.rb:14:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/static.rb:126:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-2.0.7/lib/rack/sendfile.rb:111:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/host_authorization.rb:83:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/webpacker-4.0.7/lib/webpacker/dev_server_proxy.rb:29:in `perform_request'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/rack-proxy-0.6.5/lib/rack/proxy.rb:57:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/railties-6.0.0/lib/rails/engine.rb:526:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/puma-3.12.1/lib/puma/configuration.rb:227:in `call'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/puma-3.12.1/lib/puma/server.rb:660:in `handle_request'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/puma-3.12.1/lib/puma/server.rb:474:in `process_client'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/puma-3.12.1/lib/puma/server.rb:334:in `block in run'",
                "/Users/ryanme/.rvm/gems/ruby-2.5.0/gems/puma-3.12.1/lib/puma/thread_pool.rb:135:in `block in spawn_thread'"
              ]
            },
            "data": {}
          }
        },
        "exception": {
          "stacktrace": [
            "Error: 500: Internal Server Error",
            "    at RemoteGraphQLDataSource.<anonymous> (/Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:94:25)",
            "    at Generator.next (<anonymous>)",
            "    at /Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:7:71",
            "    at new Promise (<anonymous>)",
            "    at __awaiter (/Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:3:12)",
            "    at RemoteGraphQLDataSource.errorFromResponse (/Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:84:16)",
            "    at RemoteGraphQLDataSource.<anonymous> (/Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:67:34)",
            "    at Generator.next (<anonymous>)",
            "    at /Users/ryanme/src/ap-apollo-integration-example/gateway/node_modules/@apollo/gateway/dist/datasources/RemoteGraphQLDataSource.js:7:71",
            "    at new Promise (<anonymous>)"
          ]
        }
      }
    }
  ],
  "data": {
    "customers": [
      {
        "id": "1",
        "name": "Ryan",
        "schedules": null
      },
      {
        "id": "2",
        "name": "Jackie",
        "schedules": null
      }
    ],
    "schedules": [
      {
        "id": "1",
        "time": "tomorrow",
        "customer": {
          "id": "1",
          "name": "Ryan",
          "schedules": [
            {
              "id": "1"
            },
            {
              "id": "2"
            }
          ]
        }
      },
      {
        "id": "2",
        "time": "now",
        "customer": {
          "id": "2",
          "name": "Jackie",
          "schedules": [
            {
              "id": "1"
            },
            {
              "id": "2"
            }
          ]
        }
      }
    ]
  }
}

I managed to replicate the error just on the scheduling service with the following query

{
  _entities(representations: [{__typename: "Customer", id: "1"}, {__typename:"Customer", id: "2"}]) {
    __typename
  }
}

I am running rails 6 with the latest version of graphql-ruby and apollo-federation-ruby.

# types/customer_type.rb
module Types
  class CustomerType < Types::BaseObject
    extend_type
    key fields: 'id'

    field :id, ID, null: false, external: true
    field :schedules, [ScheduleType], null: true

    # I simplified this method temporarily while testing
    def schedules
      [
        {
          id: 1,
          customer_id: 1,
          time: "today"
        },
        {
          id: 2,
          customer_id: 1,
          time: "tommorrow"
        }
      ]
    end
  end
end
# types/customer_type.rb
module Types
  class ScheduleType < Types::BaseObject
    key fields: 'id'

    field :id, ID, null: false
    field :time, String, null: false
    field :customer, Types::CustomerType, null: true

    def self.resolve_reference(reference, context)
      Schedule.find { |schedule| schedule[:id] == reference[:id] }
    end

    def customer
      { __typename: 'Customer', id: object[:customer_id] }
    end
  end
end
# schema
class SchedulingServiceSchema < GraphQL::Schema
  include ApolloFederation::Schema

  mutation(Types::MutationType)
  query(Types::QueryType)

  orphan_types Types::CustomerType
end
module Types
  class QueryType < Types::BaseObject
    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    # TODO: remove me
    field :test_field, String, null: false,
      description: "An example field added by the generator"
    def test_field
      "Hello World!"
    end

    field :schedules, [ScheduleType], null: false

    def schedules
      Schedule.all
    end
  end
end

I have 4 other services written in nodejs that are connected to my data gateway and running fine, just trying to get the ruby one to work. Any help would be appreciated.

Readme Beta Comment

πŸ‘‹ Hello, thank you for this fantastic contribution to graphql ruby.

Is this system actually in "beta" mode as the README state? I have used this in a relatively complex scenario and it seems to be fully spec compliant.

If that is the case, would you accept a PR to remove the beta declaration out of the README?

Thanks again!

Field '_service' doesn't exist on type 'Query'

I've configured my Rails app per documentation and now trying to get the schema through my gateway build in Node.js, but got:

{ message: 'Field \'_service\' doesn\'t exist on type \'Query\'',
  locations: [ { line: 1, column: 30 } ],
  path: [ 'query GetServiceDefinition', '_service' ],
  extensions:
   { code: 'undefinedField',
     typeName: 'Query',
     fieldName: '_service' } } 0 [ { message: 'Field \'_service\' doesn\'t exist on type \'Query\'',
    locations: [ [Object] ],
    path: [ 'query GetServiceDefinition', '_service' ],
    extensions:
     { code: 'undefinedField',
       typeName: 'Query',
       fieldName: '_service' } } ]

The gateway is working fine with other federated apps, either build in rails or Node.js.

Enabling federated tracing breaks multiplex

In a Rails app with a controller that supports query-batching via multiplex, enabling federated tracing (using the documented steps) leads to a runtime error when processing batched queries.

Repro

Controller action:

  def execute
    context = {
      tracing_enabled: ApolloFederation::Tracing.should_add_traces(request.headers),
    }

    multi = params[:_json]
    result =
      if multi
        MySchema.multiplex(
          multi.map do |param|
            {
              query: param[:query],
              operation_name: param[:operationName],
              variables: prepare_variables(param[:variables]),
              context: context
            }
          end
        )
      else
        MySchema.execute(
          params[:query],
          operation_name: params[:operationName],
          variables: prepare_variables(params[:variables]),
          context: context
        )
      end

    render json: result
  end

The following requests succeed:

# single query
$ curl localhost:3000/graphql -H 'content-type: application/json' -d '{"query":"query{__schema{__typename}}"}'
{"data":{"__schema":{"__typename":"__Schema"}}}

# batch of 1
$ curl localhost:3000/graphql -H 'content-type: application/json' -d '[{"query":"query{__schema{__typename}}"}]'
[{"data":{"__schema":{"__typename":"__Schema"}}}]

The following request fails:

# batch of 2
$ curl localhost:3000/graphql -H 'content-type: application/json' -d '[{"query":"query{__schema{__typename}}"},{"query":"query{__schema{__typename}}"}]'
Completed 500 Internal Server Error in 1831ms (Allocations: 7241)

NoMethodError (undefined method `context' for nil:NilClass):

apollo-federation (3.0.0) lib/apollo-federation/tracing/tracer.rb:87:in `execute_query_lazy'
apollo-federation (3.0.0) lib/apollo-federation/tracing/tracer.rb:48:in `trace'
graphql (2.0.5) lib/graphql/tracing.rb:82:in `call_tracers'
graphql (2.0.5) lib/graphql/tracing.rb:66:in `trace'
graphql (2.0.5) lib/graphql/execution/interpreter.rb:71:in `sync_lazies'
graphql (2.0.5) lib/graphql/execution/interpreter.rb:32:in `finish_multiplex'
graphql (2.0.5) lib/graphql/execution/multiplex.rb:88:in `block (3 levels) in run_all'
graphql (2.0.5) lib/graphql/dataloader.rb:181:in `block in run'
graphql (2.0.5) lib/graphql/dataloader.rb:303:in `block in spawn_fiber'

Relevant code

I don't know the inner workings well enough to connect all the dots, but here a few relevant pieces of code I plucked from the backtrace.

query = data.fetch(:query)
return result unless query.context && query.context[:tracing_enabled]

Note that in the error case, data[:query] is nil (whereas data[:multiplex] is present).

https://github.com/rmosolgo/graphql-ruby/blob/42337a85aacf7f2010500e11f57fd6b8d58e1cb1/lib/graphql/execution/interpreter.rb#L61-L62

  if query.nil? && multiplex.queries.length == 1
    query = multiplex.queries[0]
  end

This seems to explain why the error is not thrown for a batch of 1.

Compatibility with Mac M1's

When trying to do rails:db migrate, I encounter this error message due to apollo-federation gem:

rails aborted!
LoadError: cannot load such file -- google/protobuf_c

google/protobuf is the issue, but not sure how to solve this
protocolbuffers/protobuf#8062

Improve reference resolving performance

Currently only one reference can be loaded at a time, and because the type is attached to the context, lazy objects can't be used.

Some ideas:

  1. Figure out how to use lazy objects, or a different way of resolving types in ApolloFederation::Entity.resolve_type.
  2. Allow each GraphQL type to define a .resolve_references(references, context) method that can load multiple references at the same time.

I think idea 2 would be pretty straight forward to implement and a quick stopgap until idea 1 can be implemented.

Any thoughts?

Help as a maintainer

Hey Gusto,

Thanks for the awesome gem. What is the process to get involved with maintaining the gem? A few things that I think i could help with

  • Cutting new releases
  • Merging PRs for docs
  • Prepping the gem to support gateway 2 that is coming out and the changes there

Thanks!

Support for .define-style schemas

From the known issues and limitations section in the README:

  • Currently only works with class-based schemas

It makes perfect sense that you'd implement this for class-based schemas first, since that seems to be the future of graphql-ruby. Are there any plans to support the .define-style schemas as well?

I have a large .define-style schema and I'd love to be able to use this gem with it!

What is causing intermittent ApolloFederation::Tracing::NotInstalledError?

In my schema I use use ApolloFederation::Tracing.

My controller method looks like this:

  def execute
    context = {
      request: request,
      current_user: current_user,
      tracing_enabled: ApolloFederation::Tracing.should_add_traces(request.headers),
    }

    if params[:_json]
      queries = params[:_json].map do |param|
        {
          query: param[:query],
          operation_name: param[:operationName],
          variables: ensure_hash(param[:variables]),
          context: context,
        }
      end
      result = ApiSchema.multiplex(queries)
    else
      result = ApiSchema.execute(
        params[:query],
        operation_name: params[:operationName],
        variables: ensure_hash(params[:variables]),
        context: context,
      )
    end

    render json: ApolloFederation::Tracing.attach_trace_to_result(result), root: false
  end

Every now and then I see a ApolloFederation::Tracing::NotInstalledError raised. At first I thought maybe it's the multiplexing that causing issues, but the errors are tripped for a single query.

I can also confirm that there are queries getting detailed traces in Apollo Graph Manager, so some of them are definitely working.

Querying SDL Raises Exception in 1.0.1

The error is unknown keywords: request, current_user, auth, __pro_access_strategy__

Reported from:

/gem_bag/ruby/2.5.0/gems/apollo-federation-1.0.1/lib/apollo-federation/schema.rb:50:in `federation_sdl'
/gem_bag/ruby/2.5.0/gems/apollo-federation-1.0.1/lib/apollo-federation/service_field.rb:13:in `_service'

The query is:

query GetServiceDefinition { _service { sdl } }

Which is fired automatically from @apollo/gateway.

Fairly certain it is from #45.

Add support for google-protobuf v3.15.3 and up?

Just found out that the tracing part of this gem is not compatible with google-protobuf v3.15.3 and up. It can be reproduced by updating the version to 3.15.3 in

spec.add_runtime_dependency 'google-protobuf', '~> 3.7'
, bundle update google-protobuf, and then run rspec spec/apollo-federation/tracing_spec.rb.

We would then get the following error:

  1) ApolloFederation::Tracing with the legacy runtime behaves like a basic tracer respecting options on context adds the extensions.ftv1 when the context has tracing_enabled: true
     Failure/Error: ROOT_KEY => ApolloFederation::Tracing::Node.new,

     NoMethodError:
       undefined method `new' for ApolloFederation::Object:Module
     Shared Example Group: "a basic tracer" called from ./spec/apollo-federation/tracing_spec.rb:573
     # eval:1:in `initialize'
     # ./lib/apollo-federation/tracing/node_map.rb:21:in `initialize'
     # ./lib/apollo-federation/tracing/node_map.rb:21:in `new'
     # ./lib/apollo-federation/tracing/node_map.rb:21:in `initialize'
     # ./lib/apollo-federation/tracing/tracer.rb:76:in `new'
     # ./lib/apollo-federation/tracing/tracer.rb:76:in `start_trace'
     # ./lib/apollo-federation/tracing/tracer.rb:61:in `block in execute_multiplex'
     # ./lib/apollo-federation/tracing/tracer.rb:61:in `each'
     # ./lib/apollo-federation/tracing/tracer.rb:61:in `execute_multiplex'
     # ./lib/apollo-federation/tracing/tracer.rb:46:in `trace'

The specs are passing for google-protobuf v3.15.2 so I'm guessing it might be caused by this commit protocolbuffers/protobuf@9879f42#diff-b9138194ffe9e7c8bb6d79d1ed56259553d18d9cb60b66e3ba5aa2e5b078055a. I believe this is the only commit between 3.15.2 and 3.15.3.

I'm not familiar with protobuf at all so any hints on how this could be fixed will be greatly appreciated. Thanks!

v3.4.x updates federation directive version to 2.3, fails schema checks with Apollo Studio

Howdy! We updated this gem to 3.4.1 in one of our subgraphs, and its schema verification is failing in Apollo Studio. Here are some results:

Generated Schema snippet:

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"])

type Something @federation__extends @federation__key(fields: "id") {
  id: ID! @federation__external
  ...
}

Apollo Studio Check output:

UNKNOWN_FEDERATION_LINK_VERSION
[x-service] Invalid version v2.3 for the federation feature in @link direction on schema
INVALID_GRAPHQL
[x-service] Unknown directive "@federation__external".
INVALID_GRAPHQL
[x-service] Unknown directive "@federation__extends".
INVALID_GRAPHQL
[x-service] Unknown directive "@federation__key".

Our supergraph is configured in Apollo Studio to use Federation version 2.0.

Our subgraphs also specify the Federation version using this gem. It seems like that version specification is being ignored when generating the @link directive.

I'm looking at this code change πŸ‘€ c7b987d#diff-48d1e38dfab06335bc60255c48ef179d40143d14e29cfb52673b258346799900R64

Shouldn't that be honoring the federation_version value?

Using a dataloader inside `resolve_reference` does not work

It's important that when a subgraph service receives an _entities query from the gateway that it can resolve all of the references efficiently. You would expect that using a data loader inside the resolve_reference method for each entity would achieve this but unfortunately it does not work as expected. Currently the data loader is being invoked for each individual reference rather than as a single batch.

I've created a small example application here: https://github.com/col/apollo-federation-example

Expected Output:

Product.resolve_reference reference: {:__typename=>"Product", :id=>"1"}
Product.resolve_reference reference: {:__typename=>"Product", :id=>"2"}
ProductsByUpc.fetch ["1", "2"]

Actual Output:

Product.resolve_reference reference: {:__typename=>"Product", :id=>"1"}
ProductsByUpc.fetch ["1"]
Product.resolve_reference reference: {:__typename=>"Product", :id=>"2"}
ProductsByUpc.fetch ["2"]

At scale this becomes a significant problem as we can receive hundreds of references in a single _entities query.

Remove support for GraphQL 1.9.x

  • It creates additional complexity in this codebase
  • It's pretty old at this point and has a lot of breaking changes compared to GraphQL 1.13.x

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 πŸ’œ

Tracing improvements

Tracking some feedback from Apollo on the java implementation that's also relevant to this implementation.

  • Use Base64.strict_encode64

    This leaves out newlines in the middle which aren't particularly helpful when encoded in a JSON string, and causes Node to have to use a slightly slower decode implementation.

  • Capture parse and validation errors and attach them to the root node

passing `@federation__shareable` to type PageInfo

Hello Everyone πŸ˜€,

We are using type PageInfo in two subgraphs using federation 2.0

  • Currently we are getting the below error when trying to use connection_type
  • I've tried passing shareable: true also.
  • Any ideas ?
# query_type.rb
#... 
  field :users, UserType.connection_type, 'List all users.'  #, shareable: true

    def users
      User.all
    end
INVALID_FIELD_SHARING

Non-shareable field "PageInfo.endCursor" is resolved from multiple subgraphs: it is resolved from subgraphs "channels" and "users" and defined as non-shareable in all of them

INVALID_FIELD_SHARING

Non-shareable field "PageInfo.hasNextPage" is resolved from multiple subgraphs: it is resolved from subgraphs "channels" and "users" and defined as non-shareable in all of them
# base_connection.rb
module Types
  class BaseConnection < Types::BaseObject
    # add `nodes` and `pageInfo` fields, as well as `edge_type(...)` and `node_nullable(...)` overrides
    include GraphQL::Types::Relay::ConnectionBehaviors
  end
end

Support for graphql 1.10+

Hey there!

I've just started playing with this gem, so far so good, tks for getting this released 🍺 While I was experimenting with things, I tried to update to the recently released graphql 1.10.0 but found out that this gem is not compatible with it.

Was there a specific reason for locking the dep to 1.9.x? I haven't gone through the sources of this project yet and there are some breaking changes on 1.10.0 (and it is also supposed to yield faster boot times, which is why we were considering upgrading it ⚑ )

Support for federated tracing

Tracing works differently in a federated system than it does for a standard GraphQL server. It's officially documented here.

Question 1: do you think this library should include support for generating the trace proto and attaching it to the response? It could be a separately library, but I think it makes sense to include all federation concerns in one place.

Question 2: is anyone working on this?

The node.js implementation is here:

There's some unfinished ruby work for standard single-server traces (that are sent directly to Apollo's traces ingress endpoint instead of attached to the response to the gateway) here.

The automated release is failing 🚨

🚨 The automated release from the main branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the main branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


No gem API key specified.

A gem host API key must be created and set in the GEM_HOST_API_KEY environment variable on you CI environment.

You can retrieve an API key either from your ~/.gem/credentials file or in your profile in RubyGems.org.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

Creating service and gateway in Rails

Has anyone had any success building and running federated services and a gateway in a Rails app? I already have federated my schema and am trying to get each GraphQLServer running for each service.

CI badge says "No builds"

  • The branch is wrong, badge is pinned to master where our default branch is main
  • main branch builds seem to be failing πŸ˜„

feat: Explore using GraphQL Ruby's client directives

GraphQL Ruby has support for client directives now. It could potentially reduce the surface area of this library by quite a bit.

We have quite a bit code that seems to be responsible for generating the SDL.

Consider snake_case for fields

While implementing the federation spec for a few types, I got tripped up when referencing fields in the key helper and resolve_reference. Since all of the fields in graphql-ruby use snake case, I sometimes forget that the helpers for federation use the actual camelCased field names.

When authoring this library, was snake_case considered, and if so were there any reasons to not use it? I feel as though the mix of snake & camel case may be slightly confusing, what are your thoughts?

unable to configure federation directive namespace

In federation version 2 the directives have namespaces (federation__ by default). Currently this prefix is non-configurable which may cause issues when other subgraphs use a different namespace or import them without a namespace.

In Apollo studio I got the following error:

The federation "@inaccessible" directive is imported with mismatched name between subgraphs: it is imported as "@inaccessible" in subgraphs "X" and "Y" but "@federation__inaccessible" in subgraphs "A" and "B"

In this case, A and B are ruby applications, and X and Y are not.

It can be partially fixed by configuring the graph as v1 and overriding Schema#federation_dsl to prepend an import for all directives. But it's not very elegant 😬

Cannot use new interpreter with apollo-federation-ruby

Using the new graphql-ruby interpreter with apollo-federation-ruby does not work.

  • Query: { _service { sdl } }
  • Error: Field '_service' doesn't exist on type 'Query'

Schema

class AppSchema < GraphQL::Schema
  include ApolloFederation::Schema

  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST

  # ...
end

Versions

  • graphql-ruby: 1.10.7
  • apollo-federation-ruby: 1.0.4

Note: The new interpreter + apollo-federation-ruby did work with graphql-ruby 1.9.19.

support @composeDirective

Apollo Federation v2.1 introduced new @composeDirective directive that allows users to specify directives that should be preserved in the supergraph composition (by default composition strips out most directives from supergraph).

extend schema
  @link(
    url: "https://specs.apollo.dev/federation/v2.3",
    import: ["@composeDirective", "@extends", "@external", "@key", "@inaccessible", "@interfaceObject", "@override", "@provides", "@requires", "@shareable", "@tag"]
  )
  @link(url: "https://myspecs.dev/myCustomDirective/v1.0", import: ["@custom"])
  @composeDirective(name: "@custom")

// will be present in the supergraph definition
directive @custom on OBJECT

type Product @custom @key(fields: "id")
  id: ID!
  // other fields omitted for clarity
}

Additional resources:


New directive functionality can be tested using Apollo Federation Subgraph Compatibility NPX script (and Github Action). Example integration project is already provided in the subgraph compatibility testing repository.

Do you have any Federation questions? Join Apollo Discord community.

Add `resolvable` argument to `key` directive

Apollo documentation describes how to reference an entity without contributing fields. The resolvable argument on the key field indicates to Apollo that the subgraph is only referencing the entity and has no resolver.

I looked at the key implementation here and didn't see a way set this argument in my subgraph.

Could this be added in order to fulfill this aspect of federation?

Schema Example

type Product @key(fields: "id", resolvable: false) {
  id: ID!
}

Graphql 1.13.4+

Hello Gusto,

I'm facing an issue when using the latest Graphql gem version.

It can be reproduced by changing the graphql version to (1.13.4 or 1.13.5)
rspec ./spec/apollo-federation/entities_field_spec.rb:72

Thanks

Generate federated schema

Hi all,
I want to generate a federated schema. But I get the schema without any directives/etc.

for instance, I have

class UsersSchema < GraphQL::Schema
  include ApolloFederation::Schema

  orphan_types Types::UserType
end

module Types
  class UserType < Types::BaseObject
    extend_type
    key fields: 'id'
    field :id, ID, null: false, external: true
  end
end

when I use a general way to generate schema into SDL, JSON, I get an incorrect federated schema

type User {
  id: ID!
}

type Query {
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

scalar _Any

union _Entity = User

"""
The sdl representing the federated service capabilities. Includes federation
directives, removes federation types, and includes rest of full schema after
schema directives have been applied
"""
type _Service {
  sdl: String
}

How can I generate a federated schema?

Has anyone been able to integrate this library with rails?

I'm trying to integrate this library to my rails apps in graphql, apparently everything is fine, but the gateway gives an error syntax Error: Expected Name, found"} ".

from the console of my rails applications, I don't see much detail to help me

this same gateway file, the poor thing with other graphql services made in nodejs and everything fine

Production release?

I see the gem is still in the beta stage. Is there a roadmap to release? We would really like to leverage this in a production capacity :)

Question around code in docs.

In the docs you have the following

class BaseObject < GraphQL::Schema::Object
  include ApolloFederation::Object

  field_class BaseField # what is this for?
end

What is the purpose of the second last line?

The automated release is failing 🚨

🚨 The automated release from the main branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the main branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


Cannot push to the Git repository.

semantic-release cannot push the version tag to the branch main on the remote Git repository with URL https://[secure]@github.com/Gusto/apollo-federation-ruby.git.

This can be caused by:


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.