Skip to content

Commit

Permalink
federation: add support for Apollo Federation subgraph spec v2.3 (#1661)
Browse files Browse the repository at this point in the history
* federation: add support for Apollo Federation subgraph spec v2.3

Updates `federation` module to support Apollo Federation subgraph spec v2.3

Changes:
* v2.2 - update `@shareable` definition to be repeatable to allow annotating both types and their extensions (NOTE: this functionality is not applicable to `graphql-kotlin`)
* v2.3 - adds new `@interfaceObject` directive that allows you to extend interface entity functionality in subgraphs, i.e. by applying `@interfaceObject` directive on a type we provide meta information to the composition logic that this entity type is actually an interface in the supergraph. This allows us to extend interface functionality without knowing any of its implementing types.

* fix integration tests

* update remaining fed2.1 references with fed2.3

* update composition test to use latest router

* update test schema to fed 2.3

* fix directive definitions

* fix tests

* fix definitions

* add missing intf object IT
  • Loading branch information
dariuszkuc authored Feb 16, 2023
1 parent be7d676 commit 10e9d84
Show file tree
Hide file tree
Showing 26 changed files with 449 additions and 101 deletions.
10 changes: 0 additions & 10 deletions examples/federation/compose-router.yaml

This file was deleted.

27 changes: 0 additions & 27 deletions examples/federation/compose-subgraphs.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion examples/federation/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
router:
image: ghcr.io/apollographql/router:v1.2.1
image: ghcr.io/apollographql/router:v1.10.1
volumes:
- ./router.yaml:/dist/config/router.yaml
- ./supergraph.graphql:/dist/config/supergraph.graphql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 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 All @@ -20,13 +20,18 @@ 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_NAME
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_NAME
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE_V2
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL
import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE_V2
Expand Down Expand Up @@ -75,7 +80,9 @@ open class FederatedSchemaGeneratorHooks(
private val validator = FederatedSchemaValidator()

private val federationV2OnlyDirectiveNames: Set<String> = setOf(
COMPOSE_DIRECTIVE_NAME,
INACCESSIBLE_DIRECTIVE_NAME,
INTERFACE_OBJECT_DIRECTIVE_NAME,
LINK_DIRECTIVE_NAME,
OVERRIDE_DIRECTIVE_NAME,
SHAREABLE_DIRECTIVE_NAME
Expand All @@ -91,9 +98,10 @@ open class FederatedSchemaGeneratorHooks(
private val federatedDirectiveV2List: List<GraphQLDirective> = listOf(
COMPOSE_DIRECTIVE_TYPE,
EXTENDS_DIRECTIVE_TYPE,
EXTERNAL_DIRECTIVE_TYPE,
EXTERNAL_DIRECTIVE_TYPE_V2,
INACCESSIBLE_DIRECTIVE_TYPE,
KEY_DIRECTIVE_TYPE,
INTERFACE_OBJECT_DIRECTIVE_TYPE,
KEY_DIRECTIVE_TYPE_V2,
LINK_DIRECTIVE_TYPE,
OVERRIDE_DIRECTIVE_TYPE,
PROVIDES_DIRECTIVE_TYPE,
Expand All @@ -117,23 +125,19 @@ open class FederatedSchemaGeneratorHooks(
willGenerateFederatedDirective(directiveInfo)
}

private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation) =
if (federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName)) {
throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName)
} else if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) {
KEY_DIRECTIVE_TYPE
} else {
super.willGenerateDirective(directiveInfo)
}
private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when {
federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName) -> throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName)
EXTERNAL_DIRECTIVE_NAME == directiveInfo.effectiveName -> EXTERNAL_DIRECTIVE_TYPE
KEY_DIRECTIVE_NAME == directiveInfo.effectiveName -> KEY_DIRECTIVE_TYPE
else -> super.willGenerateDirective(directiveInfo)
}

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)
}
private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when (directiveInfo.effectiveName) {
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2
KEY_DIRECTIVE_NAME -> KEY_DIRECTIVE_TYPE_V2
LINK_DIRECTIVE_NAME -> LINK_DIRECTIVE_TYPE
else -> super.willGenerateDirective(directiveInfo)
}

override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType {
validator.validateGraphQLType(generatedType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.directives

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
Expand Down Expand Up @@ -32,7 +48,7 @@ import graphql.schema.GraphQLNonNull
* 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") {
* schema @composeDirective(name: "@myDirective") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
* query: Query
* }
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 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 All @@ -21,7 +21,11 @@ import graphql.introspection.Introspection.DirectiveLocation

/**
* ```graphql
* # federation v1 definition
* directive @external on FIELD_DEFINITION
*
* # federation v2 definition
* directive @external on OBJECT | FIELD_DEFINITION
* ```
*
* The @external directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. @external
Expand Down Expand Up @@ -60,7 +64,7 @@ import graphql.introspection.Introspection.DirectiveLocation
@GraphQLDirective(
name = EXTERNAL_DIRECTIVE_NAME,
description = EXTERNAL_DIRECTIVE_DESCRIPTION,
locations = [DirectiveLocation.FIELD_DEFINITION]
locations = [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION]
)
annotation class ExternalDirective

Expand All @@ -72,3 +76,9 @@ internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.
.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)
.build()
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.directives

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
import graphql.introspection.Introspection

/**
* ```graphql
* directive @interfaceObject on OBJECT
* ```
*
* This directive provides meta information to the router that this entity type defined within this subgraph is an interface in the supergraph. This allows you to extend functionality
* of an interface across the supergraph without having to implement (or even be aware of) all its implementing types.
*
* Example:
* Given an interface that is defined in another subgraph
*
* ```graphql
* interface Product @key(fields: "id") {
* id: ID!
* description: String
* }
*
* type Book implements Product @key(fields: "id") {
* id: ID!
* description: String
* pages: Int!
* }
*
* type Movie implements Product @key(fields: "id") {
* id: ID!
* description: String
* duration: Int!
* }
* ```
*
* We can extend Product entity in our subgraph and a new field directly to it. This will result in making this new field available to ALL implementing types.
*
* ```kotlin
* @InterfaceObjectDirective
* data class Product(val id: ID) {
* fun reviews(): List<Review> = TODO()
* }
* ```
*
* Which generates the following subgraph schema
*
* ```graphql
* type Product @key(fields: "id") @interfaceObject {
* id: ID!
* reviews: [Review!]!
* }
* ```
*/
@GraphQLDirective(
name = INTERFACE_OBJECT_DIRECTIVE_NAME,
description = INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION,
locations = [Introspection.DirectiveLocation.OBJECT]
)
annotation class InterfaceObjectDirective

internal const val INTERFACE_OBJECT_DIRECTIVE_NAME = "interfaceObject"
private const val INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION = "Provides meta information to the router that this entity type is an interface in the supergraph."

internal val INTERFACE_OBJECT_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(INTERFACE_OBJECT_DIRECTIVE_NAME)
.description(INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION)
.validLocations(Introspection.DirectiveLocation.OBJECT)
.build()
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 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 All @@ -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.1"
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.3"

/**
* ```graphql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 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 All @@ -21,7 +21,7 @@ import graphql.introspection.Introspection.DirectiveLocation

/**
* ```graphql
* directive @shareable on FIELD_DEFINITION | OBJECT
* directive @shareable repeatable on FIELD_DEFINITION | OBJECT
* ```
*
* Shareable directive indicates that given object and/or field can be resolved by multiple subgraphs. If an object is marked as `@shareable` then all its fields are automatically shareable without the
Expand All @@ -44,6 +44,7 @@ import graphql.introspection.Introspection.DirectiveLocation
* }
* ```
*/
@Repeatable
@GraphQLDirective(
name = SHAREABLE_DIRECTIVE_NAME,
description = SHAREABLE_DIRECTIVE_DESCRIPTION,
Expand All @@ -58,4 +59,5 @@ internal val SHAREABLE_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql
.name(SHAREABLE_DIRECTIVE_NAME)
.description(SHAREABLE_DIRECTIVE_DESCRIPTION)
.validLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT)
.repeatable(true)
.build()
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class FederatedSchemaV2GeneratorTest {
fun `verify can generate federated schema`() {
val expectedSchema =
"""
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
query: Query
}
Expand All @@ -49,7 +49,7 @@ class FederatedSchemaV2GeneratorTest {
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
"Marks location within schema as inaccessible from the GraphQL Gateway"
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
Expand All @@ -60,6 +60,9 @@ class FederatedSchemaV2GeneratorTest {
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Provides meta information to the router that this entity type is an interface in the supergraph."
directive @interfaceObject on OBJECT
"Space separated list of primary keys needed to access federated object"
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
Expand All @@ -76,7 +79,7 @@ class FederatedSchemaV2GeneratorTest {
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
"Indicates that given object and/or field can be resolved by multiple subgraphs"
directive @shareable on OBJECT | FIELD_DEFINITION
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
"Directs the executor to skip this field or fragment when the `if` argument is true."
directive @skip(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.composeDirective

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
Expand Down
Loading

0 comments on commit 10e9d84

Please sign in to comment.