Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for @composeDirective #253

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def merge_directives(node, type)

def directive_name(directive)
if schema.federation_2? && !Schema::IMPORTED_DIRECTIVES.include?(directive[:name])
"#{schema.link_namespace}__#{directive[:name]}"
"#{schema.default_link_namespace}__#{directive[:name]}"
else
directive[:name]
end
Expand Down
60 changes: 51 additions & 9 deletions lib/apollo-federation/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
module ApolloFederation
module Schema
IMPORTED_DIRECTIVES = ['inaccessible', 'tag'].freeze
IMPORTED_DIRECTIVES_V2_1 = ['composeDirective'].freeze

def self.included(klass)
klass.extend(CommonMethods)
Expand All @@ -16,9 +17,16 @@ def self.included(klass)
module CommonMethods
DEFAULT_LINK_NAMESPACE = 'federation'

def federation(version: '1.0', link: {})
def federation(version: '1.0', default_link_namespace: nil, links: [], compose_directives: [])
@federation_version = version
@link = { as: DEFAULT_LINK_NAMESPACE }.merge(link)
@default_link_namespace = default_link_namespace
@links = links

if !federation_2_1? && compose_directives.any?
raise ArgumentError, 'composeDirective is available in Federation 2.1 and later'
end

@compose_directives = compose_directives
end

def federation_version
Expand All @@ -29,6 +37,10 @@ def federation_2?
Gem::Version.new(federation_version.to_s) >= Gem::Version.new('2.0.0')
end

def federation_2_1?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should add specific minor version support. We do have some cleanup to do, but it's going to be easier to maintain this lib if we simply aim to support the latest version of federation instead of incrementally supporting minor versions of 2.x. See this issue for more context.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the context. I removed this check, mutualized imported directives and fixed tests. I'll wait your call on how we plan to clean up versions

Gem::Version.new(federation_version.to_s) >= Gem::Version.new('2.1.0')
end

def federation_sdl(context: nil)
document_from_schema = FederatedDocumentFromSchemaDefinition.new(self, context: context)

Expand All @@ -37,8 +49,8 @@ def federation_sdl(context: nil)
output
end

def link_namespace
@link ? @link[:as] : find_inherited_value(:link_namespace)
def default_link_namespace
@default_link_namespace || find_inherited_value(:default_link_namespace, DEFAULT_LINK_NAMESPACE)
end

def query(new_query_object = nil)
Expand All @@ -62,14 +74,44 @@ def original_query
@orig_query_object || find_inherited_value(:original_query)
end

def compose_directives
@compose_directives || find_inherited_value(:compose_directives, [])
end

def links
@links || find_inherited_value(:links, [])
end

def all_links
imported_directives = IMPORTED_DIRECTIVES
imported_directives += IMPORTED_DIRECTIVES_V2_1 if federation_2_1?
default_link = {
url: 'https://specs.apollo.dev/federation/v2.3',
import: imported_directives,
}
default_link[:as] = default_link_namespace if default_link_namespace != DEFAULT_LINK_NAMESPACE
[default_link, *links]
end

def federation_2_prefix
federation_namespace = ", as: \"#{link_namespace}\"" if link_namespace != DEFAULT_LINK_NAMESPACE
schema = ['extend schema']

all_links.each do |link|
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This printing logic is fairly complex. Can we make it easier to read?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to do make it easier to read but not a ruby expert, please tell me if there's a better way :)

link_str = " @link(url: \"#{link[:url]}\""
link_str += ", as: \"#{link[:as]}\"" if link[:as]
link_str += ", import: [#{link[:import].map { |d| "\"@#{d}\"" }.join(', ')}]" if link[:import]
link_str += ')'
schema << link_str
end

compose_directives.each do |directive|
schema << " @composeDirective(name: \"@#{directive}\")"
end

<<~SCHEMA
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3"#{federation_namespace}, import: [#{(IMPORTED_DIRECTIVES.map { |directive| "\"@#{directive}\"" }).join(', ')}])
schema << ''
schema << ''

SCHEMA
schema.join("\n")
end

def schema_entities
Expand Down
114 changes: 103 additions & 11 deletions spec/apollo-federation/service_field_v2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,98 @@ def execute_sdl(schema)
)
end

it 'returns the federation SDL with multiple links for the schema' do
product = Class.new(base_object) do
graphql_name 'Product'

field :upc, String, null: false
end

query_obj = Class.new(base_object) do
graphql_name 'Query'

field :product, product, null: true
end

schema = Class.new(base_schema) do
query query_obj
federation version: '2.3',
links: [{
grxy marked this conversation as resolved.
Show resolved Hide resolved
url: 'https://specs.example.com/federation/v2.3',
import: ['test'],
}]
end

expect(execute_sdl(schema)).to match_sdl(
<<~GRAPHQL,
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@tag", "@composeDirective"])
@link(url: "https://specs.example.com/federation/v2.3", import: ["@test"])

type Product {
upc: String!
}

type Query {
product: Product
}
GRAPHQL
)
end

it 'returns the federation SDL with compose directives for the schema' do
complexity_directive = Class.new(GraphQL::Schema::Directive) do
graphql_name 'complexity'
argument :fixed, Integer, required: true
description 'complexity of the field'
locations GraphQL::Schema::Directive::FIELD_DEFINITION
end

product = Class.new(base_object) do
graphql_name 'Product'

field :upc, String, null: false
end

query_obj = Class.new(base_object) do
graphql_name 'Query'

field :product, product, null: true, directives: { complexity_directive => { fixed: 1 } }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is always going to fail on older GraphQL ruby versions. I think we have a couple options potentially:

  1. Drop support for GraphQL Ruby < 1.12 (or maybe even < 2.0)
  2. Only run these tests on newer versions
  • If we do this, we should add code to make sure that we warn or raise if composeDirective is used on an older GraphQL Ruby version

I lean towards the first option.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the first option as well. Do you plan to drop support for GraphQL Ruby < 1.12 in a separate PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@utay Yeah we'd probably want to do it in a separate PR and publish a new major version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grxy Any plan to do that in the near future? It was the main blocker here I think

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @utay I'll have to defer to the folks who have taken over maintenance on this gem. CC: @sethc2 @slauppy @sofie-c @simoncoffin

end

schema = Class.new(base_schema) do
query query_obj
federation version: '2.3',
links: [{
url: 'https://specs.example.com/federation/v2.3',
import: [complexity_directive.graphql_name],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one probably doesn't need importing with an @link since it is being defined inline.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like even if it's being defined inline it needs to be referenced via a custom link. For example, if I try to compose a supergraph with an inline directive that isn't imported with link I get:

UNKNOWN: Directive "@..." in subgraph "..." cannot be composed because it is not a member of a core feature

In the docs they're doing it similarly

}],
compose_directives: [complexity_directive.graphql_name]
end

expect(execute_sdl(schema)).to match_sdl(
<<~GRAPHQL,
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@tag", "@composeDirective"])
@link(url: "https://specs.example.com/federation/v2.3", import: ["@complexity"])
@composeDirective(name: "@complexity")

"""
complexity of the field
"""
directive @complexity(fixed: Int!) on FIELD_DEFINITION

type Product {
upc: String!
}

type Query {
product: Product @complexity(fixed: 1)
}
GRAPHQL
)
end

it 'returns valid SDL for type extensions' do
product = Class.new(base_object) do
graphql_name 'Product'
Expand Down Expand Up @@ -291,7 +383,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
query query_obj
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand Down Expand Up @@ -327,7 +419,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
query query_obj
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand Down Expand Up @@ -364,7 +456,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
query query_obj
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand Down Expand Up @@ -401,7 +493,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
query query_obj
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand Down Expand Up @@ -437,7 +529,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
query query_obj
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand Down Expand Up @@ -468,7 +560,7 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
orphan_types product
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
Expand All @@ -495,13 +587,13 @@ def execute_sdl(schema)

schema = Class.new(base_schema) do
orphan_types product
federation version: '2.3', link: { as: 'fed2' }
federation version: '2.3', default_link_namespace: 'fed2'
end

expect(execute_sdl(schema)).to match_sdl(
<<~GRAPHQL,
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", as: "fed2", import: ["@inaccessible", "@tag"])
@link(url: "https://specs.apollo.dev/federation/v2.3", as: "fed2", import: ["@inaccessible", "@tag", "@composeDirective"])

type Product @fed2__interfaceObject @fed2__key(fields: "id") {
id: ID!
Expand Down Expand Up @@ -1421,7 +1513,7 @@ def execute_sdl(schema)
expect(execute_sdl(schema)).to match_sdl(
<<~GRAPHQL,
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@tag"])
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@tag", "@composeDirective"])

type Product @federation__interfaceObject @federation__key(fields: "id") {
id: ID!
Expand Down Expand Up @@ -1973,7 +2065,7 @@ def self.visible?(context)
end

new_base_schema = Class.new(base_schema) do
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
end

schema = Class.new(new_base_schema) do
Expand Down Expand Up @@ -2011,7 +2103,7 @@ def self.visible?(context)
end

new_base_schema = Class.new(base_schema) do
federation version: '2.0', link: { as: 'fed2' }
federation version: '2.0', default_link_namespace: 'fed2'
query query_obj
end

Expand Down