Skip to content

Commit

Permalink
feat: Support Federation v2 (#196)
Browse files Browse the repository at this point in the history
* Add support for the @Shareable directive

* Fix rubocop offense

* Add support for the @inaccessible directive

* Add support for the @OverRide directive

* Refactor to appease rubocop

* Replace `Hash#except` with `Hash#delete_if`

Oops! I didn't realise `Hash#except` was only available from Ruby 3.0

* Update README to include v2.0 directives

* Fix incorrect handling of keyword args in field

* Import federation directives into subgraph to opt into federation 2 (hack!)

See: https://www.apollographql.com/docs/federation/federation-2/moving-to-federation-2/#opt-in-to-federation-2
This is a horrible hack. Need to figure out if there is a clean way of including this in the document and the printer output.

* Enable federation 2 via a `federation_2` class method on Schema

* Change `federation_2` to `federation version: 2`

This is more explicit and extendable in the future if we ever need any other federation options at the schema level

* Add a unit test for Schema.federation_version

* Update README with how to opt in to Federation v2

* Update federation version to support semantic versioning

* Add `federation__` namespace prefix to all directives when using federation version >= 2.0

* Refactor the `merge_directives` method

This approach feel a little easier to understand

* Fix grammar issues in test names
  • Loading branch information
col authored Jun 21, 2022
1 parent dd7943c commit 238736c
Show file tree
Hide file tree
Showing 9 changed files with 1,035 additions and 7 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ class MySchema < GraphQL::Schema
end
```

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

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

## Example

The [`example`](./example/) folder contains a Ruby implementation of Apollo's [`federation-demo`](https://github.com/apollographql/federation-demo). To run it locally, install the Ruby dependencies:
Expand Down Expand Up @@ -160,6 +169,59 @@ end
```
See [field set syntax](#field-set-syntax) for more details on the format of the `fields` option.

### The `@shareable` directive (Apollo Federation v2)

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#shareable)

Call `shareable` within your class definition:

```ruby
class User < BaseObject
shareable
end
```

Pass the `shareable: true` option to your field definition:

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

### The `@inaccessible` directive (Apollo Federation v2)

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible)

Call `inaccessible` within your class definition:

```ruby
class User < BaseObject
inaccessible
end
```

Pass the `inaccessible: true` option to your field definition:

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

### The `@override` directive (Apollo Federation v2)

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#override)

Pass the `override:` option to your field definition:

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

### Field set syntax

Field sets can be either strings encoded with the Apollo Field Set [syntax]((https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#scalar-_fieldset)) or arrays, hashes and snake case symbols that follow the graphql-ruby conventions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ def query_type?(type)

def merge_directives(node, type)
if type.is_a?(ApolloFederation::HasDirectives)
directives = type.federation_directives
directives = type.federation_directives || []
else
directives = []
end

(directives || []).each do |directive|
directives.each do |directive|
node = node.merge_directive(
name: directive[:name],
name: schema.federation_2? ? "federation__#{directive[:name]}" : directive[:name],
arguments: build_arguments_node(directive[:arguments]),
)
end
Expand Down
47 changes: 44 additions & 3 deletions lib/apollo-federation/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,29 @@ module ApolloFederation
module Field
include HasDirectives

def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &block)
VERSION_1_DIRECTIVES = %i[external requires provides].freeze
VERSION_2_DIRECTIVES = %i[shareable inaccessible override].freeze

def initialize(*args, **kwargs, &block)
add_v1_directives(**kwargs)
add_v2_directives(**kwargs)

# Remove the custom kwargs
kwargs = kwargs.delete_if do |k, _|
VERSION_1_DIRECTIVES.include?(k) || VERSION_2_DIRECTIVES.include?(k)
end

# Pass on the default args:
super(*args, **kwargs, &block)
end

private

def add_v1_directives(external: nil, requires: nil, provides: nil, **_kwargs)
if external
add_directive(name: 'external')
end

if requires
add_directive(
name: 'requires',
Expand All @@ -23,6 +42,7 @@ def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &
],
)
end

if provides
add_directive(
name: 'provides',
Expand All @@ -36,8 +56,29 @@ def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &
)
end

# Pass on the default args:
super(*args, **kwargs, &block)
nil
end

def add_v2_directives(shareable: nil, inaccessible: nil, override: nil, **_kwargs)
if shareable
add_directive(name: 'shareable')
end

if inaccessible
add_directive(name: 'inaccessible')
end

if override
add_directive(
name: 'override',
arguments: [
name: 'from',
values: override[:from],
],
)
end

nil
end
end
end
4 changes: 4 additions & 0 deletions lib/apollo-federation/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def extend_type
add_directive(name: 'extends')
end

def inaccessible
add_directive(name: 'inaccessible')
end

def key(fields:, camelize: true)
add_directive(
name: 'key',
Expand Down
8 changes: 8 additions & 0 deletions lib/apollo-federation/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ def extend_type
add_directive(name: 'extends')
end

def shareable
add_directive(name: 'shareable')
end

def inaccessible
add_directive(name: 'inaccessible')
end

def key(fields:, camelize: true)
add_directive(
name: 'key',
Expand Down
23 changes: 22 additions & 1 deletion lib/apollo-federation/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,30 @@ def self.included(klass)
end

module CommonMethods
FEDERATION_2_PREFIX = <<~SCHEMA
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0")
SCHEMA

def federation(version: '1.0')
@federation_version = version
end

def federation_version
@federation_version || '1.0'
end

def federation_2?
Gem::Version.new(federation_version.to_s) >= Gem::Version.new('2.0.0')
end

def federation_sdl(context: nil)
document_from_schema = FederatedDocumentFromSchemaDefinition.new(self, context: context)
GraphQL::Language::Printer.new.print(document_from_schema.document)

output = GraphQL::Language::Printer.new.print(document_from_schema.document)
output.prepend(FEDERATION_2_PREFIX) if federation_2?
output
end

def query(new_query_object = nil)
Expand Down
73 changes: 73 additions & 0 deletions spec/apollo-federation/schema_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require 'spec_helper'
require 'graphql'
require 'apollo-federation/schema'

RSpec.describe ApolloFederation::Schema do
describe '.federation_version' do
it 'returns 1.0 by default' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
end

expect(schema.federation_version).to eq('1.0')
end

it 'returns the specified version when set' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: '2.0'
end

expect(schema.federation_version).to eq('2.0')
end
end

describe '.federation_2?' do
it 'returns false when version is an integer less than 2.0' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: 1
end

expect(schema.federation_2?).to be(false)
end

it 'returns false when version is less than 2.0' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: '1.5'
end

expect(schema.federation_2?).to be(false)
end

it 'returns true when the version is an integer equal to 2' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: 2
end

expect(schema.federation_2?).to be(true)
end

it 'returns true when the version is a float equal to 2.0' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: 2.0
end

expect(schema.federation_2?).to be(true)
end

it 'returns true when the version is a string greater than 2.0' do
schema = Class.new(GraphQL::Schema) do
include ApolloFederation::Schema
federation version: '2.0.1'
end

expect(schema.federation_2?).to be(true)
end
end
end
Loading

0 comments on commit 238736c

Please sign in to comment.