Skip to content

Commit 238736c

Browse files
authored
feat: Support Federation v2 (#196)
* 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
1 parent dd7943c commit 238736c

File tree

9 files changed

+1035
-7
lines changed

9 files changed

+1035
-7
lines changed

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ class MySchema < GraphQL::Schema
6161
end
6262
```
6363

64+
**Optional:** To opt in to Federation v2, specify the version in your schema:
65+
66+
```ruby
67+
class MySchema < GraphQL::Schema
68+
include ApolloFederation::Schema
69+
federation version: '2.0'
70+
end
71+
```
72+
6473
## Example
6574

6675
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:
@@ -160,6 +169,59 @@ end
160169
```
161170
See [field set syntax](#field-set-syntax) for more details on the format of the `fields` option.
162171

172+
### The `@shareable` directive (Apollo Federation v2)
173+
174+
[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#shareable)
175+
176+
Call `shareable` within your class definition:
177+
178+
```ruby
179+
class User < BaseObject
180+
shareable
181+
end
182+
```
183+
184+
Pass the `shareable: true` option to your field definition:
185+
186+
```ruby
187+
class User < BaseObject
188+
field :id, ID, null: false, shareable: true
189+
end
190+
```
191+
192+
### The `@inaccessible` directive (Apollo Federation v2)
193+
194+
[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible)
195+
196+
Call `inaccessible` within your class definition:
197+
198+
```ruby
199+
class User < BaseObject
200+
inaccessible
201+
end
202+
```
203+
204+
Pass the `inaccessible: true` option to your field definition:
205+
206+
```ruby
207+
class User < BaseObject
208+
field :id, ID, null: false, inaccessible: true
209+
end
210+
```
211+
212+
### The `@override` directive (Apollo Federation v2)
213+
214+
[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#override)
215+
216+
Pass the `override:` option to your field definition:
217+
218+
```ruby
219+
class Product < BaseObject
220+
field :id, ID, null: false
221+
field :inStock, Boolean, null: false, override: { from: 'Products' }
222+
end
223+
```
224+
163225
### Field set syntax
164226

165227
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:

lib/apollo-federation/federated_document_from_schema_definition.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ def query_type?(type)
5555

5656
def merge_directives(node, type)
5757
if type.is_a?(ApolloFederation::HasDirectives)
58-
directives = type.federation_directives
58+
directives = type.federation_directives || []
5959
else
6060
directives = []
6161
end
6262

63-
(directives || []).each do |directive|
63+
directives.each do |directive|
6464
node = node.merge_directive(
65-
name: directive[:name],
65+
name: schema.federation_2? ? "federation__#{directive[:name]}" : directive[:name],
6666
arguments: build_arguments_node(directive[:arguments]),
6767
)
6868
end

lib/apollo-federation/field.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,29 @@ module ApolloFederation
77
module Field
88
include HasDirectives
99

10-
def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &block)
10+
VERSION_1_DIRECTIVES = %i[external requires provides].freeze
11+
VERSION_2_DIRECTIVES = %i[shareable inaccessible override].freeze
12+
13+
def initialize(*args, **kwargs, &block)
14+
add_v1_directives(**kwargs)
15+
add_v2_directives(**kwargs)
16+
17+
# Remove the custom kwargs
18+
kwargs = kwargs.delete_if do |k, _|
19+
VERSION_1_DIRECTIVES.include?(k) || VERSION_2_DIRECTIVES.include?(k)
20+
end
21+
22+
# Pass on the default args:
23+
super(*args, **kwargs, &block)
24+
end
25+
26+
private
27+
28+
def add_v1_directives(external: nil, requires: nil, provides: nil, **_kwargs)
1129
if external
1230
add_directive(name: 'external')
1331
end
32+
1433
if requires
1534
add_directive(
1635
name: 'requires',
@@ -23,6 +42,7 @@ def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &
2342
],
2443
)
2544
end
45+
2646
if provides
2747
add_directive(
2848
name: 'provides',
@@ -36,8 +56,29 @@ def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &
3656
)
3757
end
3858

39-
# Pass on the default args:
40-
super(*args, **kwargs, &block)
59+
nil
60+
end
61+
62+
def add_v2_directives(shareable: nil, inaccessible: nil, override: nil, **_kwargs)
63+
if shareable
64+
add_directive(name: 'shareable')
65+
end
66+
67+
if inaccessible
68+
add_directive(name: 'inaccessible')
69+
end
70+
71+
if override
72+
add_directive(
73+
name: 'override',
74+
arguments: [
75+
name: 'from',
76+
values: override[:from],
77+
],
78+
)
79+
end
80+
81+
nil
4182
end
4283
end
4384
end

lib/apollo-federation/interface.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def extend_type
1818
add_directive(name: 'extends')
1919
end
2020

21+
def inaccessible
22+
add_directive(name: 'inaccessible')
23+
end
24+
2125
def key(fields:, camelize: true)
2226
add_directive(
2327
name: 'key',

lib/apollo-federation/object.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ def extend_type
1616
add_directive(name: 'extends')
1717
end
1818

19+
def shareable
20+
add_directive(name: 'shareable')
21+
end
22+
23+
def inaccessible
24+
add_directive(name: 'inaccessible')
25+
end
26+
1927
def key(fields:, camelize: true)
2028
add_directive(
2129
name: 'key',

lib/apollo-federation/schema.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,30 @@ def self.included(klass)
1212
end
1313

1414
module CommonMethods
15+
FEDERATION_2_PREFIX = <<~SCHEMA
16+
extend schema
17+
@link(url: "https://specs.apollo.dev/federation/v2.0")
18+
19+
SCHEMA
20+
21+
def federation(version: '1.0')
22+
@federation_version = version
23+
end
24+
25+
def federation_version
26+
@federation_version || '1.0'
27+
end
28+
29+
def federation_2?
30+
Gem::Version.new(federation_version.to_s) >= Gem::Version.new('2.0.0')
31+
end
32+
1533
def federation_sdl(context: nil)
1634
document_from_schema = FederatedDocumentFromSchemaDefinition.new(self, context: context)
17-
GraphQL::Language::Printer.new.print(document_from_schema.document)
35+
36+
output = GraphQL::Language::Printer.new.print(document_from_schema.document)
37+
output.prepend(FEDERATION_2_PREFIX) if federation_2?
38+
output
1839
end
1940

2041
def query(new_query_object = nil)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'graphql'
5+
require 'apollo-federation/schema'
6+
7+
RSpec.describe ApolloFederation::Schema do
8+
describe '.federation_version' do
9+
it 'returns 1.0 by default' do
10+
schema = Class.new(GraphQL::Schema) do
11+
include ApolloFederation::Schema
12+
end
13+
14+
expect(schema.federation_version).to eq('1.0')
15+
end
16+
17+
it 'returns the specified version when set' do
18+
schema = Class.new(GraphQL::Schema) do
19+
include ApolloFederation::Schema
20+
federation version: '2.0'
21+
end
22+
23+
expect(schema.federation_version).to eq('2.0')
24+
end
25+
end
26+
27+
describe '.federation_2?' do
28+
it 'returns false when version is an integer less than 2.0' do
29+
schema = Class.new(GraphQL::Schema) do
30+
include ApolloFederation::Schema
31+
federation version: 1
32+
end
33+
34+
expect(schema.federation_2?).to be(false)
35+
end
36+
37+
it 'returns false when version is less than 2.0' do
38+
schema = Class.new(GraphQL::Schema) do
39+
include ApolloFederation::Schema
40+
federation version: '1.5'
41+
end
42+
43+
expect(schema.federation_2?).to be(false)
44+
end
45+
46+
it 'returns true when the version is an integer equal to 2' do
47+
schema = Class.new(GraphQL::Schema) do
48+
include ApolloFederation::Schema
49+
federation version: 2
50+
end
51+
52+
expect(schema.federation_2?).to be(true)
53+
end
54+
55+
it 'returns true when the version is a float equal to 2.0' do
56+
schema = Class.new(GraphQL::Schema) do
57+
include ApolloFederation::Schema
58+
federation version: 2.0
59+
end
60+
61+
expect(schema.federation_2?).to be(true)
62+
end
63+
64+
it 'returns true when the version is a string greater than 2.0' do
65+
schema = Class.new(GraphQL::Schema) do
66+
include ApolloFederation::Schema
67+
federation version: '2.0.1'
68+
end
69+
70+
expect(schema.federation_2?).to be(true)
71+
end
72+
end
73+
end

0 commit comments

Comments
 (0)