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(federation): federation v2.6 support #1928

Merged
merged 2 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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.29.1
image: ghcr.io/apollographql/router:v1.40.0
volumes:
- ./router.yaml:/dist/config/router.yaml
- ./supergraph.graphql:/dist/config/supergraph.graphql
Expand Down
2 changes: 1 addition & 1 deletion examples/federation/supergraph.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
federation_version: =2.5.4
federation_version: =2.6.3
subgraphs:
products:
routing_url: http://products:8080/graphql
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 @@ -43,6 +43,7 @@ import com.expediagroup.graphql.generator.federation.directives.LinkDirective
import com.expediagroup.graphql.generator.federation.directives.LinkImport
import com.expediagroup.graphql.generator.federation.directives.LinkedSpec
import com.expediagroup.graphql.generator.federation.directives.OVERRIDE_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.POLICY_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
Expand All @@ -52,10 +53,12 @@ import com.expediagroup.graphql.generator.federation.directives.SHAREABLE_DIRECT
import com.expediagroup.graphql.generator.federation.directives.TAG_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.keyDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.linkDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.policyDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.providesDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.requiresDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.requiresScopesDirectiveType
import com.expediagroup.graphql.generator.federation.directives.toAppliedLinkDirective
import com.expediagroup.graphql.generator.federation.directives.toAppliedPolicyDirective
import com.expediagroup.graphql.generator.federation.directives.toAppliedRequiresScopesDirective
import com.expediagroup.graphql.generator.federation.exception.DuplicateSpecificationLinkImport
import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage
Expand All @@ -69,6 +72,7 @@ import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_NAME
import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.FieldSetTransformer
import com.expediagroup.graphql.generator.federation.types.LINK_IMPORT_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.SCOPE_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.SERVICE_FIELD_DEFINITION
import com.expediagroup.graphql.generator.federation.types._Service
Expand Down Expand Up @@ -149,6 +153,18 @@ open class FederatedSchemaGeneratorHooks(
}
}
}
private val policiesScalar: GraphQLScalarType by lazy {
POLICY_SCALAR_TYPE.run {
val policyScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
if (policyScalarName != this.name) {
this.transform {
it.name(policyScalarName)
}
} else {
this
}
}
}
private val scopesScalar: GraphQLScalarType by lazy {
SCOPE_SCALAR_TYPE.run {
val scopesScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
Expand Down Expand Up @@ -252,17 +268,24 @@ open class FederatedSchemaGeneratorHooks(
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2
KEY_DIRECTIVE_NAME -> keyDirectiveDefinition(fieldSetScalar)
LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar)
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar)
PROVIDES_DIRECTIVE_NAME -> providesDirectiveDefinition(fieldSetScalar)
REQUIRES_DIRECTIVE_NAME -> requiresDirectiveDefinition(fieldSetScalar)
REQUIRES_SCOPE_DIRECTIVE_NAME -> requiresScopesDirectiveType(scopesScalar)
else -> super.willGenerateDirective(directiveInfo)
}

override fun willApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLAppliedDirective? {
return if (directiveInfo.effectiveName == REQUIRES_SCOPE_DIRECTIVE_NAME) {
directive.toAppliedRequiresScopesDirective(directiveInfo)
} else {
super.willApplyDirective(directiveInfo, directive)
return when (directiveInfo.effectiveName) {
REQUIRES_SCOPE_DIRECTIVE_NAME -> {
directive.toAppliedRequiresScopesDirective(directiveInfo)
}
POLICY_DIRECTIVE_NAME -> {
directive.toAppliedPolicyDirective(directiveInfo)
}
else -> {
super.willApplyDirective(directiveInfo, directive)
}
}
}

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 @@ -32,7 +32,7 @@ const val LINK_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$LINK_SPEC"
const val LINK_SPEC_LATEST_URL = "$LINK_SPEC_URL_PREFIX/v$LINK_SPEC_LATEST_VERSION"

const val FEDERATION_SPEC = "federation"
const val FEDERATION_SPEC_LATEST_VERSION = "2.5"
const val FEDERATION_SPEC_LATEST_VERSION = "2.6"
const val FEDERATION_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$FEDERATION_SPEC"
const val FEDERATION_SPEC_LATEST_URL = "$FEDERATION_SPEC_URL_PREFIX/v$FEDERATION_SPEC_LATEST_VERSION"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.
* 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.GraphQLIgnore

/**
* Annotation representing authorization policy scalar type that is used by the `@policy directive.
*
* @param value required authorization policy
* @see [com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE]
*/
@LinkedSpec(FEDERATION_SPEC)
annotation class Policy(val value: String)

// this is a workaround for JVM lack of support nested arrays as annotation values
@GraphQLIgnore
annotation class Policies(val value: Array<Policy>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.
* 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 com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
import graphql.introspection.Introspection
import graphql.schema.GraphQLAppliedDirective
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLList
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLScalarType
import kotlin.reflect.full.memberProperties

/**
* ```graphql
* directive @policy(scopes: [[Policy!]!]!) on
* ENUM
* | FIELD_DEFINITION
* | INTERFACE
* | OBJECT
* | SCALAR
* ```
*
*
* Directive that is used to indicate that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor.
* Refer to the <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router article</a> for additional details.
*
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives#policy">@policy definition</a>
* @see <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router @policy documentation</a>
*/
@LinkedSpec(FEDERATION_SPEC)
@Repeatable
@GraphQLDirective(
name = POLICY_DIRECTIVE_NAME,
description = POLICY_DIRECTIVE_DESCRIPTION,
locations = [
Introspection.DirectiveLocation.ENUM,
Introspection.DirectiveLocation.FIELD_DEFINITION,
Introspection.DirectiveLocation.INTERFACE,
Introspection.DirectiveLocation.OBJECT,
Introspection.DirectiveLocation.SCALAR,
]
)
annotation class PolicyDirective(val policies: Array<Policies>)

internal const val POLICY_DIRECTIVE_NAME = "policy"
private const val POLICY_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor"
private const val POLICIES_ARGUMENT = "policies"

internal fun policyDirectiveDefinition(policies: GraphQLScalarType): graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(POLICY_DIRECTIVE_NAME)
.description(POLICY_DIRECTIVE_DESCRIPTION)
.validLocations(
Introspection.DirectiveLocation.ENUM,
Introspection.DirectiveLocation.FIELD_DEFINITION,
Introspection.DirectiveLocation.INTERFACE,
Introspection.DirectiveLocation.OBJECT,
Introspection.DirectiveLocation.SCALAR
)
.argument(
GraphQLArgument.newArgument()
.name(POLICIES_ARGUMENT)
.type(
GraphQLNonNull.nonNull(
GraphQLList.list(
GraphQLNonNull(
GraphQLList.list(
policies
)
)
)
)
)
)
.build()

@Suppress("UNCHECKED_CAST")
internal fun graphql.schema.GraphQLDirective.toAppliedPolicyDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective {
// we need to manually transform @policy directive definition as JVM does not support nested array as annotation arguments
val annotationPolicies = directiveInfo.directive.annotationClass.memberProperties
.first { it.name == POLICIES_ARGUMENT }
.call(directiveInfo.directive) as? Array<Policies> ?: emptyArray()
val policies = annotationPolicies.map { policiesAnnotation -> policiesAnnotation.value.toList() }

return this.toAppliedDirective()
.transform { appliedDirectiveBuilder ->
this.getArgument(POLICIES_ARGUMENT)
.toAppliedArgument()
.transform { argumentBuilder ->
argumentBuilder.valueProgrammatic(policies)
}
.let {
appliedDirectiveBuilder.argument(it)
}
}
}
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 @@ -73,7 +73,7 @@ internal fun requiresScopesDirectiveType(scopes: GraphQLScalarType): graphql.sch
)
.argument(
GraphQLArgument.newArgument()
.name("scopes")
.name(SCOPES_ARGUMENT)
.type(
GraphQLNonNull.nonNull(
GraphQLList.list(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.
* 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.types

import com.expediagroup.graphql.generator.federation.directives.Policy
import com.expediagroup.graphql.generator.federation.exception.CoercingValueToLiteralException
import graphql.GraphQLContext
import graphql.Scalars
import graphql.execution.CoercedVariables
import graphql.language.StringValue
import graphql.language.Value
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import java.util.Locale

internal const val POLICY_SCALAR_NAME = "Policy"

/**
* Custom scalar type that is used to represent authentication policy which serializes as a String.
*/
internal val POLICY_SCALAR_TYPE: GraphQLScalarType = GraphQLScalarType.newScalar(Scalars.GraphQLString)
.name(POLICY_SCALAR_NAME)
.description("Federation type representing authorization policy")
.coercing(PolicyCoercing)
.build()

private object PolicyCoercing : Coercing<Policy, String> {
override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String =
when (dataFetcherResult) {
is Policy -> dataFetcherResult.value
else -> throw CoercingSerializeException(
"Cannot serialize $dataFetcherResult. Expected type 'Policy' but was '${dataFetcherResult.javaClass.simpleName}'."
)
}

override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Policy =
when (input) {
is Policy -> input
is StringValue -> Policy::class.constructors.first().call(input.value)
else -> throw CoercingParseLiteralException(
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
)
}

override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Policy =
when (input) {
is StringValue -> Policy::class.constructors.first().call(input.value)
else -> throw CoercingParseLiteralException(
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
)
}

override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> =
when (input) {
is Policy -> StringValue.newStringValue(input.value).build()
else -> throw CoercingValueToLiteralException(Policy::class, input)
}
}
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 : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}

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 @@ -43,7 +43,7 @@ class ComposeDirectiveTest {
fun `verify we can generate valid schema with @composeDirective`() {
val expectedSchema =
"""
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ContactDirectiveTest {
fun `verify we can import federation spec using custom @link`() {
val expectedSchema =
"""
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.5"){
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class LinkDirectiveTest {
fun `verify we can import federation spec using custom @link`() {
val expectedSchema =
"""
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.5"){
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}

Expand Down
Loading
Loading