Skip to content

Commit

Permalink
[federation] add support for Fed 2.1 (#1591)
Browse files Browse the repository at this point in the history
By default, Supergraph schema excludes all custom directives. Federation 2.1 adds new `@composeDirective` that can be used to instruct composition logic to preserve custom directives in the Supergraph schema.

```graphql
directive @composeDirective(name: String!) repeatable on SCHEMA
```
  • Loading branch information
dariuszkuc committed Nov 10, 2022
1 parent 533dc03 commit 7b40e7d
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.genera
import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDLV2
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL
Expand Down Expand Up @@ -88,6 +89,7 @@ open class FederatedSchemaGeneratorHooks(
REQUIRES_DIRECTIVE_TYPE
)
private val federatedDirectiveV2List: List<GraphQLDirective> = listOf(
COMPOSE_DIRECTIVE_TYPE,
EXTENDS_DIRECTIVE_TYPE,
EXTERNAL_DIRECTIVE_TYPE,
INACCESSIBLE_DIRECTIVE_TYPE,
Expand Down Expand Up @@ -127,6 +129,8 @@ open class FederatedSchemaGeneratorHooks(
private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation) =
if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) {
KEY_DIRECTIVE_TYPE_V2
} else if (LINK_DIRECTIVE_NAME == directiveInfo.effectiveName) {
LINK_DIRECTIVE_TYPE
} else {
super.willGenerateDirective(directiveInfo)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.expediagroup.graphql.generator.federation.directives

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
import graphql.Scalars
import graphql.introspection.Introspection
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLNonNull

/**
* ```graphql
* directive @composeDirective(name: String!) repeatable on SCHEMA
* ```
*
* By default, Supergraph schema excludes all custom directives. The `@composeDirective` is used to specify custom directives that should be exposed in the Supergraph schema.
*
* Example:
* Given `@custom` directive we can preserve it in the Supergraph schema
*
* ```kotlin
* @GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
* annotation class CustomDirective
*
* @ComposeDirective(name = "custom")
* class CustomSchema
*
* class SimpleQuery {
* @CustomDirective
* fun helloWorld(): String = "Hello World"
* }
* ```
*
* it will generate following schema
*
* ```graphql
* schema @composeDirective(name: "@myDirective") @link(import : ["composeDirective", "extends", "external", "inaccessible", "key", "override", "provides", "requires", "shareable", "tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1") {
* query: Query
* }
*
* directive @custom on FIELD_DEFINITION
*
* type Query {
* helloWorld: String! @custom
* }
* ```
*
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective">@composeDirective definition</a>
*/
@Repeatable
@GraphQLDirective(
name = COMPOSE_DIRECTIVE_NAME,
description = COMPOSE_DIRECTIVE_DESCRIPTION,
locations = [Introspection.DirectiveLocation.SCHEMA]
)
annotation class ComposeDirective(val name: String)

internal const val COMPOSE_DIRECTIVE_NAME = "composeDirective"
private const val COMPOSE_DIRECTIVE_DESCRIPTION = "Marks underlying custom directive to be included in the Supergraph schema"

internal val COMPOSE_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(COMPOSE_DIRECTIVE_NAME)
.description(COMPOSE_DIRECTIVE_DESCRIPTION)
.validLocations(Introspection.DirectiveLocation.SCHEMA)
.argument(
GraphQLArgument.newArgument()
.name("name")
.type(GraphQLNonNull.nonNull(Scalars.GraphQLString))
)
.repeatable(true)
.build()
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import graphql.schema.GraphQLList
import graphql.schema.GraphQLNonNull

const val LINK_SPEC_URL = "https://specs.apollo.dev/link/v1.0/"
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.0"
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.1"

/**
* ```graphql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ class FederatedSchemaV2GeneratorTest {
fun `verify can generate federated schema`() {
val expectedSchema =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
"Marks the field, argument, input field or enum value as deprecated"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.expediagroup.graphql.generator.federation.data.integration.composeDirective

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
import com.expediagroup.graphql.generator.federation.directives.ComposeDirective
import graphql.introspection.Introspection

@ComposeDirective(name = "custom")
class CustomSchema

@GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
annotation class CustomDirective

class SimpleQuery {
@CustomDirective
fun helloWorld(): String = "Hello World"
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,13 @@ scalar CustomScalar"""

const val BASE_SERVICE_SDL =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks target object as extending part of the federated schema"
directive @extends on OBJECT | INTERFACE
Expand Down Expand Up @@ -142,10 +145,13 @@ scalar FieldSet

const val FEDERATED_SERVICE_SDL_V2 =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
"Marks target object as extending part of the federated schema"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.expediagroup.graphql.generator.federation.validation.integration

import com.expediagroup.graphql.generator.TopLevelObject
import com.expediagroup.graphql.generator.extensions.print
import com.expediagroup.graphql.generator.federation.data.integration.composeDirective.CustomSchema
import com.expediagroup.graphql.generator.federation.data.integration.composeDirective.SimpleQuery
import com.expediagroup.graphql.generator.federation.toFederatedSchema
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow

class ComposeDirectiveIT {

@Test
fun `verifies applying @composeDirective generates valid schema`() {
assertDoesNotThrow {
val schema = toFederatedSchema(
config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.composeDirective"),
queries = listOf(TopLevelObject(SimpleQuery())),
schemaObject = TopLevelObject(CustomSchema())
)

val expected = """
schema @composeDirective(name : "custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @custom on FIELD_DEFINITION
"Links definitions within the document to external schemas."
directive @link(import: [String], url: String!) repeatable on SCHEMA
type Query {
_service: _Service!
helloWorld: String! @custom
}
type _Service {
sdl: String!
}
""".trimIndent()
val actual = schema.print(
includeDirectivesFilter = { directive -> "link" == directive || "composeDirective" == directive || "custom" == directive },
includeScalarTypes = false
).trim()
Assertions.assertEquals(expected, actual)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,13 @@ class GraphQLGradlePluginIT : GraphQLGradlePluginAbstractIT() {

val expectedFederatedSchemaWithCustomScalar =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ internal val DEFAULT_SCHEMA =

internal val FEDERATED_SCHEMA =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ class GenerateSDLMojoTest {
assertTrue(schemaFile.exists(), "schema file was generated")

val expectedSchema = """
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ class GenerateSDLMojoTest {
assertTrue(schemaFile.exists(), "schema file was generated")

val expectedSchema = """
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ class GenerateCustomSDLTest {
fun `verify we can generate SDL using custom hooks provider`() {
val expectedSchema =
"""
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}
"Marks underlying custom directive to be included in the Supergraph schema"
directive @composeDirective(name: String!) repeatable on SCHEMA
"Marks the field, argument, input field or enum value as deprecated"
directive @deprecated(
"The reason for the deprecation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ toFederatedSchema(
will generate

```graphql
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
query: Query
}

directive @composeDirective(name: String!) repeatable on SCHEMA
directive @extends on OBJECT | INTERFACE
directive @external on FIELD_DEFINITION
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
Expand Down
56 changes: 56 additions & 0 deletions website/docs/schema-generator/federation/federated-directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,49 @@ title: Federated Directives

For more details, see the [Apollo Federation Specification](https://www.apollographql.com/docs/federation/federation-spec/).

## `@composeDirective` directive

```graphql
directive @composeDirective(name: String!) repeatable on SCHEMA
```

By default, Supergraph schema excludes all custom directives. The `@composeDirective` is used to specify custom directives that should be exposed in the Supergraph schema.

Example:
Given `@custom` directive we can preserve it in the Supergraph schema

```kotlin
@GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
annotation class CustomDirective

@ComposeDirective(name = "custom")
class CustomSchema

class SimpleQuery {
@CustomDirective
fun helloWorld(): String = "Hello World"
}
```

it will generate following schema

```graphql
schema
@composeDirective(name: "@myDirective")
@link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1")
{
query: Query
}

directive @custom on FIELD_DEFINITION

type Query {
helloWorld: String! @custom
}
```

See [@composeDirective definition](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective) for more information.

## `@contact` directive

```graphql
Expand Down Expand Up @@ -251,6 +294,19 @@ External schemas are identified by their `url`, which optionally ends with a nam
By default, external types should be namespaced (prefixed with `<namespace>__`, e.g. `key` directive should be namespaced as `federation__key`) unless they are explicitly imported.
`graphql-kotlin` automatically imports ALL federation directives to avoid the need for namespacing.

```kotlin
@LinkDirective(url = "https://myspecs.company.dev/foo/v1.0", imports = ["@foo", "bar"])
class MySchema
```

This will generate following schema:

```graphql
schema @link(import : ["@foo", "bar"], url : "https://myspecs.company.dev/foo/v1.0") {
query: Query
}
```

:::danger
We currently DO NOT support full `@link` directive capability as it requires support for namespacing and renaming imports. This functionality may be added in the future releases. See
[@link specification](https://specs.apollo.dev/link/v1.0) for details.
Expand Down

0 comments on commit 7b40e7d

Please sign in to comment.