apollo-federation
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
Call extend_type
within your class definition:
class User < BaseObject
extend_type
end
@key
directive
The
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.
@external
directive
The
Pass the external: true
option to your field definition:
class User < BaseObject
field :id, ID, null: false, external: true
end
@requires
directive
The
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.
@provides
directive
The
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.
@shareable
directive (Apollo Federation v2)
The
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
@inaccessible
directive (Apollo Federation v2)
The
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
@override
directive (Apollo Federation v2)
The
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
@tag
directive (Apollo Federation v2)
The
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
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:
- Add
use ApolloFederation::Tracing
to your schema class. - 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
. Sincegraphql-ruby
doesn't natively support schema directives, the directives will only be visible to the Apollo Gateway through theQuery._service
field (see the Apollo Federation specification) or viaSchema#federation_sdl
as explained above.