Skip to content

Commit

Permalink
BREAKING: drop Federation v1 support (#1978)
Browse files Browse the repository at this point in the history
### 📝 Description

Federation v2 was released almost 3 years ago. It is an evolution of the
Federation spec to make it more powerful, flexible and easier to adopt.

It is no longer possible to create Federated Supergraphs targeting v1
composition. Users still using fed v1 shoul migrate ASAP to the fed v2.

### 🔗 Related Issues

---------

Co-authored-by: Samuel Vazquez <sam_2f@hotmail.com>
  • Loading branch information
dariuszkuc and samuelAndalon committed Jun 27, 2024
1 parent 9d11d44 commit fae9be3
Show file tree
Hide file tree
Showing 20 changed files with 243 additions and 753 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import java.util.UUID
import kotlin.reflect.KType

@Component
class CustomFederatedHooks(resolvers: List<FederatedTypeResolver>) : FederatedSchemaGeneratorHooks(resolvers, true) {
class CustomFederatedHooks(resolvers: List<FederatedTypeResolver>) : FederatedSchemaGeneratorHooks(resolvers,) {
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
UUID::class -> graphqlUUIDType
ULocale::class -> graphqlULocaleType
Expand Down
2 changes: 1 addition & 1 deletion generator/graphql-kotlin-federation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ tasks {
limit {
counter = "BRANCH"
value = "COVEREDRATIO"
minimum = "0.83".toBigDecimal()
minimum = "0.82".toBigDecimal()
}
}
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ import graphql.introspection.Introspection.DirectiveLocation

/**
* ```graphql
* # federation v1 definition
* directive @external on FIELD_DEFINITION
*
* # federation v2 definition
* directive @external on OBJECT | FIELD_DEFINITION
* ```
*
Expand Down Expand Up @@ -73,12 +69,6 @@ internal const val EXTERNAL_DIRECTIVE_NAME = "external"
private const val EXTERNAL_DIRECTIVE_DESCRIPTION = "Marks target field as external meaning it will be resolved by federated schema"

internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(EXTERNAL_DIRECTIVE_NAME)
.description(EXTERNAL_DIRECTIVE_DESCRIPTION)
.validLocations(DirectiveLocation.FIELD_DEFINITION)
.build()

internal val EXTERNAL_DIRECTIVE_TYPE_V2: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(EXTERNAL_DIRECTIVE_NAME)
.description(EXTERNAL_DIRECTIVE_DESCRIPTION)
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -29,10 +29,6 @@ import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLScalarType
import graphql.schema.GraphQLSchemaElement
import graphql.schema.GraphQLTypeVisitorStub
import graphql.util.TraversalControl
import graphql.util.TraverserContext
import java.util.Locale

internal const val FIELD_SET_SCALAR_NAME = "FieldSet"
Expand Down Expand Up @@ -91,18 +87,3 @@ private object FieldSetCoercing : Coercing<FieldSet, String> {
else -> throw CoercingValueToLiteralException(FieldSet::class, input)
}
}

/**
* Renames FieldSet scalar (used in Federation V2) to _FieldSet (used in Federation V1).
*/
class FieldSetTransformer : GraphQLTypeVisitorStub() {
override fun visitGraphQLScalarType(node: GraphQLScalarType, context: TraverserContext<GraphQLSchemaElement>): TraversalControl {
if (node.name == "FieldSet") {
val legacyFieldSetScalar = node.transform {
it.name("_FieldSet")
}
return changeNode(context, legacyFieldSetScalar)
}
return super.visitGraphQLScalarType(node, context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.expediagroup.graphql.generator.federation.data.queries.simple.SimpleQ
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME
import graphql.schema.GraphQLUnionType
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import kotlin.test.assertNotNull
Expand All @@ -33,7 +34,7 @@ class FederatedSchemaGeneratorTest {
fun `verify can generate federated schema`() {
val expectedSchema =
"""
schema {
schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand All @@ -45,11 +46,8 @@ class FederatedSchemaGeneratorTest {
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
"Marks target object as extending part of the federated schema"
directive @extends on OBJECT | INTERFACE
"Marks target field as external meaning it will be resolved by federated schema"
directive @external on FIELD_DEFINITION
directive @external on OBJECT | FIELD_DEFINITION
"Directs the executor to include this field or fragment only when the `if` argument is true"
directive @include(
Expand All @@ -58,16 +56,19 @@ class FederatedSchemaGeneratorTest {
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Space separated list of primary keys needed to access federated object"
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
"Links definitions within the document to external schemas."
directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA
"Indicates an Input Object is a OneOf Input Object."
directive @oneOf on INPUT_OBJECT
"Specifies the base type field set that will be selectable by the gateway"
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
"Specifies required input field set from the base type for a resolver"
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
"Directs the executor to skip this field or fragment when the `if` argument is true."
directive @skip(
Expand All @@ -81,33 +82,33 @@ class FederatedSchemaGeneratorTest {
url: String!
) on SCALAR
interface Product @extends @key(fields : "id") @key(fields : "upc") {
id: String! @external
interface Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) {
id: String!
reviews: [Review!]!
upc: String! @external
upc: String!
}
union _Entity = Author | Book | User
type Author @extends @key(fields : "authorId") {
authorId: Int! @external
name: String! @external
type Author @key(fields : "authorId", resolvable : true) {
authorId: Int!
name: String!
}
type Book implements Product @extends @key(fields : "id") @key(fields : "upc") {
type Book implements Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) {
author: User! @provides(fields : "name")
id: String! @external
id: String!
reviews: [Review!]!
shippingCost: String! @requires(fields : "weight")
upc: String! @external
upc: String!
weight: Float! @external
}
type CustomScalar {
value: String!
}
type Query @extends {
type Query {
"Union of all types that use the @key directive, including both types native to the schema and extended types"
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
Expand All @@ -120,30 +121,31 @@ class FederatedSchemaGeneratorTest {
id: String!
}
type User @extends @key(fields : "userId") {
name: String! @external
userId: Int! @external
type User @key(fields : "userId", resolvable : true) {
name: String!
userId: Int!
}
type _Service {
sdl: String!
}
"Federation type representing set of fields"
scalar FieldSet
"Federation scalar type used to represent any external entities passed to _entities query."
scalar _Any
"Federation type representing set of fields"
scalar _FieldSet
scalar link__Import
""".trimIndent()

val config = FederatedSchemaGeneratorConfig(
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v1"),
hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false)
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated"),
hooks = FederatedSchemaGeneratorHooks(emptyList())
)

val schema = toFederatedSchema(config = config)

assertEquals(expectedSchema, schema.print().trim())
Assertions.assertEquals(expectedSchema, schema.print().trim())
val productType = schema.getObjectType("Book")
assertNotNull(productType)
assertNotNull(productType.hasAppliedDirective(KEY_DIRECTIVE_NAME))
Expand All @@ -157,7 +159,7 @@ class FederatedSchemaGeneratorTest {
fun `verify generator does not add federation queries for non-federated schemas`() {
val expectedSchema =
"""
schema {
schema @link(url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand All @@ -167,30 +169,18 @@ class FederatedSchemaGeneratorTest {
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
"Marks target object as extending part of the federated schema"
directive @extends on OBJECT | INTERFACE
"Marks target field as external meaning it will be resolved by federated schema"
directive @external on FIELD_DEFINITION
"Directs the executor to include this field or fragment only when the `if` argument is true"
directive @include(
"Included when true."
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Space separated list of primary keys needed to access federated object"
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
"Links definitions within the document to external schemas."
directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA
"Indicates an Input Object is a OneOf Input Object."
directive @oneOf on INPUT_OBJECT
"Specifies the base type field set that will be selectable by the gateway"
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
"Specifies required input field set from the base type for a resolver"
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
"Directs the executor to skip this field or fragment when the `if` argument is true."
directive @skip(
"Skipped when true."
Expand All @@ -203,7 +193,7 @@ class FederatedSchemaGeneratorTest {
url: String!
) on SCALAR
type Query @extends {
type Query {
_service: _Service!
hello(name: String!): String!
}
Expand All @@ -212,27 +202,56 @@ class FederatedSchemaGeneratorTest {
sdl: String!
}
"Federation type representing set of fields"
scalar _FieldSet
scalar link__Import
""".trimIndent()

val config = FederatedSchemaGeneratorConfig(
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.simple"),
hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false)
hooks = FederatedSchemaGeneratorHooks(emptyList())
)

val schema = toFederatedSchema(config, listOf(TopLevelObject(SimpleQuery())))
assertEquals(expectedSchema, schema.print().trim())
}

@Test
fun `verify a nested federated schema still works`() {
fun `verify a schema with self nested query still works`() {
val expectedSchema =
"""
schema {
schema @link(url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
"Directs the executor to include this field or fragment only when the `if` argument is true"
directive @include(
"Included when true."
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Links definitions within the document to external schemas."
directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA
"Indicates an Input Object is a OneOf Input Object."
directive @oneOf on INPUT_OBJECT
"Directs the executor to skip this field or fragment when the `if` argument is true."
directive @skip(
"Skipped when true."
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Exposes a URL that specifies the behaviour of this scalar."
directive @specifiedBy(
"The URL that specifies the behaviour of this scalar."
url: String!
) on SCALAR
type Query {
_service: _Service!
getSimpleNestedObject: [SelfReferenceObject]!
Expand All @@ -248,16 +267,15 @@ class FederatedSchemaGeneratorTest {
sdl: String!
}
"Federation type representing set of fields"
scalar _FieldSet
scalar link__Import
""".trimIndent()

val config = FederatedSchemaGeneratorConfig(
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.simple"),
hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false)
hooks = FederatedSchemaGeneratorHooks(emptyList())
)

val schema = toFederatedSchema(config, listOf(TopLevelObject(NestedQuery())))
assertEquals(expectedSchema, schema.print(includeDirectives = false).trim())
assertEquals(expectedSchema, schema.print(includeDirectives = true).trim())
}
}
Loading

0 comments on commit fae9be3

Please sign in to comment.