From b173ea0ec47f3d3d8185042e44e1697c441cacd8 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:02:14 -0500 Subject: [PATCH] feat: support Apollo Federation 2.5 (#1839) ### :pencil: Description Adds support for [Apollo Federation v2.5](https://www.apollographql.com/docs/federation/federation-versions#v25). Adds new `willApplyDirective`/`didApplyDirective` hooks that can be used to customize transformation of directive definition to applied directive. JVM does not support nested arrays as annotation arguments so we need to apply custom hooks to generate valid `@requiresScopes` directive. New hooks can also be used to filter out default arguments (#1830). New federation directives - `@authenticated` - indicates that target element is only accessible to the authenticated supergraph users - `@requiresScopes` - indicates that target element is only accessible to the authenticated supergraph users with the appropriate JWT scopes Note: due to the potential conflict on directive names we will no longer auto import federation directives. New directives will be auto-namespaced to the target spec. For backwards compatibility, we will continue auto-importing directives up to Federation version 2.3. ### :link: Related Issues #1830 --- .github/workflows/federation-integration.yml | 2 +- examples/federation/supergraph.yaml | 2 +- .../FederatedSchemaGeneratorHooks.kt | 26 +++++ .../directives/AuthenticatedDirective.kt | 54 +++++++++ .../federation/directives/ComposeDirective.kt | 2 +- .../federation/directives/LinkDirective.kt | 4 +- .../directives/RequiresScopesDirective.kt | 110 ++++++++++++++++++ .../generator/federation/directives/Scope.kt | 32 +++++ .../generator/federation/types/Scope.kt | 74 ++++++++++++ .../FederatedSchemaV2GeneratorTest.kt | 2 +- .../compose/ComposeDirectiveTest.kt | 2 +- .../contact/ContactDirectiveTest.kt | 2 +- .../directives/link/LinkDirectiveTest.kt | 2 +- .../RequiresScopesDirectiveTest.kt | 105 +++++++++++++++++ .../execution/ServiceQueryResolverTest.kt | 4 +- .../generator/federation/types/ScopesTest.kt | 75 ++++++++++++ .../generator/hooks/SchemaGeneratorHooks.kt | 19 ++- .../internal/types/generateDirective.kt | 36 +++--- .../integration/resources/sdl/custom.graphql | 2 +- .../resources/sdl/federated.graphql | 2 +- .../plugin/maven/GenerateSDLMojoTest.kt | 2 +- .../plugin/maven/GenerateSDLMojoTest.kt | 2 +- .../plugin/schema/GenerateCustomSDLTest.kt | 2 +- .../federation/apollo-federation.mdx | 12 +- .../federation/federated-directives.md | 35 +++++- 25 files changed, 559 insertions(+), 51 deletions(-) create mode 100644 generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/AuthenticatedDirective.kt create mode 100644 generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/RequiresScopesDirective.kt create mode 100644 generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/Scope.kt create mode 100644 generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/Scope.kt create mode 100644 generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/requiresscope/RequiresScopesDirectiveTest.kt create mode 100644 generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/types/ScopesTest.kt diff --git a/.github/workflows/federation-integration.yml b/.github/workflows/federation-integration.yml index 9dcaa42a1e..846fe7f575 100644 --- a/.github/workflows/federation-integration.yml +++ b/.github/workflows/federation-integration.yml @@ -95,7 +95,7 @@ jobs: run: ./gradlew bootJar graphqlGenerateSDL - name: Compatibility Test - uses: apollographql/federation-subgraph-compatibility@v1 + uses: apollographql/federation-subgraph-compatibility@v2 with: compose: 'docker-compose.yaml' schema: 'build/schema.graphql' diff --git a/examples/federation/supergraph.yaml b/examples/federation/supergraph.yaml index d9f5e32384..d20b882be7 100644 --- a/examples/federation/supergraph.yaml +++ b/examples/federation/supergraph.yaml @@ -1,4 +1,4 @@ -federation_version: =2.4.13 +federation_version: =2.5.4 subgraphs: products: routing_url: http://products:8080/graphql diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt index 4e0363a51d..506dbad60a 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt @@ -47,13 +47,16 @@ import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTI import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_TYPE import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_TYPE +import com.expediagroup.graphql.generator.federation.directives.REQUIRES_SCOPE_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.SHAREABLE_DIRECTIVE_NAME 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.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.toAppliedRequiresScopesDirective import com.expediagroup.graphql.generator.federation.exception.DuplicateSpecificationLinkImport import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage import com.expediagroup.graphql.generator.federation.exception.UnknownSpecificationException @@ -65,6 +68,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.SCOPE_SCALAR_TYPE import com.expediagroup.graphql.generator.federation.types.SERVICE_FIELD_DEFINITION import com.expediagroup.graphql.generator.federation.types._Service import com.expediagroup.graphql.generator.federation.types.generateEntityFieldDefinition @@ -73,6 +77,7 @@ import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks import graphql.TypeResolutionEnvironment import graphql.schema.DataFetcher import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLDirective import graphql.schema.GraphQLNamedType @@ -142,6 +147,18 @@ open class FederatedSchemaGeneratorHooks( } } } + private val scopesScalar: GraphQLScalarType by lazy { + SCOPE_SCALAR_TYPE.run { + val scopesScalarName = namespacedTypeName(FEDERATION_SPEC, this.name) + if (scopesScalarName != this.name) { + this.transform { + it.name(scopesScalarName) + } + } else { + this + } + } + } override fun willBuildSchema( queries: List, @@ -235,9 +252,18 @@ open class FederatedSchemaGeneratorHooks( LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar) 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) + } + } + override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType { validator.validateGraphQLType(generatedType) return super.didGenerateGraphQLType(type, generatedType) diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/AuthenticatedDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/AuthenticatedDirective.kt new file mode 100644 index 0000000000..0e9396be16 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/AuthenticatedDirective.kt @@ -0,0 +1,54 @@ +/* + * 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 @authenticated on + * ENUM + * | FIELD_DEFINITION + * | INTERFACE + * | OBJECT + * | SCALAR + * ``` + * + * Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the @requiresScopes directive usage. + * Refer to the Apollo Router article for additional details. + * + * @see @authenticated definition + * @see Apollo Router @authenticated documentation + */ +@LinkedSpec(FEDERATION_SPEC) +@Repeatable +@GraphQLDirective( + name = AUTHENTICATED_DIRECTIVE_NAME, + description = AUTHENTICATED_DIRECTIVE_DESCRIPTION, + locations = [ + Introspection.DirectiveLocation.ENUM, + Introspection.DirectiveLocation.FIELD_DEFINITION, + Introspection.DirectiveLocation.INTERFACE, + Introspection.DirectiveLocation.OBJECT, + Introspection.DirectiveLocation.SCALAR, + ] +) +annotation class AuthenticatedDirective + +internal const val AUTHENTICATED_DIRECTIVE_NAME = "requiresScopes" +private const val AUTHENTICATED_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is accessible only to the authenticated supergraph users" diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt index a24ba080a6..c7a7ce2bab 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt @@ -46,7 +46,7 @@ import graphql.introspection.Introspection * it will generate following schema * * ```graphql - * schema @composeDirective(name: "@custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ + * schema @composeDirective(name: "@custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){ * query: Query * } * diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt index 409a3c9587..7fe6e11320 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt @@ -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.3" +const val FEDERATION_SPEC_LATEST_VERSION = "2.5" 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" @@ -43,7 +43,7 @@ const val FEDERATION_SPEC_LATEST_URL = "$FEDERATION_SPEC_URL_PREFIX/v$FEDERATION * * The `@link` directive links definitions within the document to external schemas. * - * External schemas are identified by their url, which should end with a specification name and version with the following format: `{NAME}/v{MAJOR}.{MINOR}`, e.g. `url = "https://specs.apollo.dev/federation/v2.3"`. + * External schemas are identified by their url, which should end with a specification name and version with the following format: `{NAME}/v{MAJOR}.{MINOR}`, e.g. `url = "https://specs.apollo.dev/federation/v2.5"`. * * External types are associated with the target specification by annotating it with `@LinkedSpec` meta annotation. External types defined in the specification will be automatically namespaced * (prefixed with `{NAME}__`) unless they are explicitly imported. While both custom namespace (`as`) and import arguments are optional, due to https://github.com/ExpediaGroup/graphql-kotlin/issues/1830 diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/RequiresScopesDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/RequiresScopesDirective.kt new file mode 100644 index 0000000000..2d1bcb1228 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/RequiresScopesDirective.kt @@ -0,0 +1,110 @@ +/* + * 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 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 @requiresScopes(scopes: [[Scope!]!]!) on + * ENUM + * | FIELD_DEFINITION + * | INTERFACE + * | OBJECT + * | SCALAR + * ``` + * + * Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes. Refer to the + * Apollo Router article for additional details. + * + * @see @requiresScope definition + * @see Apollo Router @requiresScope documentation + */ +@LinkedSpec(FEDERATION_SPEC) +@Repeatable +@GraphQLDirective( + name = REQUIRES_SCOPE_DIRECTIVE_NAME, + description = REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION, + locations = [ + Introspection.DirectiveLocation.ENUM, + Introspection.DirectiveLocation.FIELD_DEFINITION, + Introspection.DirectiveLocation.INTERFACE, + Introspection.DirectiveLocation.OBJECT, + Introspection.DirectiveLocation.SCALAR, + ] +) +annotation class RequiresScopesDirective(val scopes: Array) + +internal const val REQUIRES_SCOPE_DIRECTIVE_NAME = "requiresScopes" +private const val REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes" +private const val SCOPES_ARGUMENT = "scopes" + +internal fun requiresScopesDirectiveType(scopes: GraphQLScalarType): graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() + .name(REQUIRES_SCOPE_DIRECTIVE_NAME) + .description(REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION) + .validLocations( + Introspection.DirectiveLocation.ENUM, + Introspection.DirectiveLocation.FIELD_DEFINITION, + Introspection.DirectiveLocation.INTERFACE, + Introspection.DirectiveLocation.OBJECT, + Introspection.DirectiveLocation.SCALAR + ) + .argument( + GraphQLArgument.newArgument() + .name("scopes") + .type( + GraphQLNonNull.nonNull( + GraphQLList.list( + GraphQLNonNull( + GraphQLList.list( + scopes + ) + ) + ) + ) + ) + ) + .build() + +@Suppress("UNCHECKED_CAST") +internal fun graphql.schema.GraphQLDirective.toAppliedRequiresScopesDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective { + // we need to manually transform @requiresScopes directive definition as JVM does not support nested array as annotation arguments + val annotationScopes = directiveInfo.directive.annotationClass.memberProperties + .first { it.name == SCOPES_ARGUMENT } + .call(directiveInfo.directive) as? Array ?: emptyArray() + val scopes = annotationScopes.map { scopesAnnotation -> scopesAnnotation.value.toList() } + + return this.toAppliedDirective() + .transform { appliedDirectiveBuilder -> + this.getArgument(SCOPES_ARGUMENT) + .toAppliedArgument() + .transform { argumentBuilder -> + argumentBuilder.valueProgrammatic(scopes) + } + .let { + appliedDirectiveBuilder.argument(it) + } + } +} diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/Scope.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/Scope.kt new file mode 100644 index 0000000000..d54cda5081 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/Scope.kt @@ -0,0 +1,32 @@ +/* + * 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.GraphQLIgnore + +/** + * Annotation representing JWT scope scalar type that is used by the `@requiresScope directive. + * + * @param value required JWT scope + * @see [com.expediagroup.graphql.generator.federation.types.SCOPE_SCALAR_TYPE] + */ +@LinkedSpec(FEDERATION_SPEC) +annotation class Scope(val value: String) + +// this is a workaround for JVM lack of support nested arrays as annotation values +@GraphQLIgnore +annotation class Scopes(val value: Array) diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/Scope.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/Scope.kt new file mode 100644 index 0000000000..f5d159d2f5 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/Scope.kt @@ -0,0 +1,74 @@ +/* + * 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.types + +import com.expediagroup.graphql.generator.federation.directives.Scope +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 SCOPE_SCALAR_NAME = "Scope" + +/** + * Custom scalar type that is used to represent a valid JWT scope which serializes as a String. + */ +internal val SCOPE_SCALAR_TYPE: GraphQLScalarType = GraphQLScalarType.newScalar(Scalars.GraphQLString) + .name(SCOPE_SCALAR_NAME) + .description("Federation type representing a JWT scope") + .coercing(ScopeCoercing) + .build() + +private object ScopeCoercing : Coercing { + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = + when (dataFetcherResult) { + is Scope -> dataFetcherResult.value + else -> throw CoercingSerializeException( + "Cannot serialize $dataFetcherResult. Expected type 'Scope' but was '${dataFetcherResult.javaClass.simpleName}'." + ) + } + + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Scope = + when (input) { + is Scope -> input + is StringValue -> Scope::class.constructors.first().call(input.value) + else -> throw CoercingParseLiteralException( + "Cannot parse $input to Scope. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'." + ) + } + + override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Scope = + when (input) { + is StringValue -> Scope::class.constructors.first().call(input.value) + else -> throw CoercingParseLiteralException( + "Cannot parse $input to Scope. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'." + ) + } + + override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> = + when (input) { + is Scope -> StringValue.newStringValue(input.value).build() + else -> throw CoercingValueToLiteralException(Scope::class, input) + } +} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt index 4bde92131e..5356f94ba9 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt @@ -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.3"){ + schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt index 8b536ddcd1..0bdc9bf617 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt @@ -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.3"){ + 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"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/contact/ContactDirectiveTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/contact/ContactDirectiveTest.kt index 0e8fb6e510..ba475eb109 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/contact/ContactDirectiveTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/contact/ContactDirectiveTest.kt @@ -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.3"){ + 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"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/link/LinkDirectiveTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/link/LinkDirectiveTest.kt index df60e19208..31709b12b3 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/link/LinkDirectiveTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/link/LinkDirectiveTest.kt @@ -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.3"){ + schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/requiresscope/RequiresScopesDirectiveTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/requiresscope/RequiresScopesDirectiveTest.kt new file mode 100644 index 0000000000..68d2843d48 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/requiresscope/RequiresScopesDirectiveTest.kt @@ -0,0 +1,105 @@ +/* + * 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.requiresscope + +import com.expediagroup.graphql.generator.TopLevelObject +import com.expediagroup.graphql.generator.extensions.print +import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig +import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks +import com.expediagroup.graphql.generator.federation.directives.REQUIRES_SCOPE_DIRECTIVE_NAME +import com.expediagroup.graphql.generator.federation.directives.RequiresScopesDirective +import com.expediagroup.graphql.generator.federation.directives.Scope +import com.expediagroup.graphql.generator.federation.directives.Scopes +import com.expediagroup.graphql.generator.federation.toFederatedSchema +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.test.assertNotNull + +class RequiresScopesDirectiveTest { + + @Test + fun `verify we can import federation spec using custom @link`() { + val expectedSchema = + """ + schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ + 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 + + "Indicates to composition that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes" + directive @federation__requiresScopes(scopes: [[federation__Scope]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + + "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 + + "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! + foo: String! @federation__requiresScopes(scopes : [["scope1", "scope2"], ["scope3"]]) + } + + type _Service { + sdl: String! + } + + "Federation type representing a JWT scope" + scalar federation__Scope + + scalar link__Import + """.trimIndent() + + val config = FederatedSchemaGeneratorConfig( + supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.requiresscope"), + hooks = FederatedSchemaGeneratorHooks(emptyList()) + ) + + val schema = toFederatedSchema(queries = listOf(TopLevelObject(FooQuery())), config = config) + Assertions.assertEquals(expectedSchema, schema.print().trim()) + val query = schema.getObjectType("Query") + assertNotNull(query) + val fooQuery = query.getField("foo") + assertNotNull(fooQuery) + assertNotNull(fooQuery.hasAppliedDirective(REQUIRES_SCOPE_DIRECTIVE_NAME)) + } + + class FooQuery { + @RequiresScopesDirective(scopes = [Scopes([Scope("scope1"), Scope("scope2")]), Scopes([Scope("scope3")])]) + fun foo(): String = TODO() + } +} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt index 2fd9b1b5c9..09a1641c4d 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt @@ -90,7 +90,7 @@ scalar CustomScalar""" const val BASE_SERVICE_SDL = """ -schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ +schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } @@ -118,7 +118,7 @@ scalar link__Import const val FEDERATED_SERVICE_SDL_V2 = """ -schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ +schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/types/ScopesTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/types/ScopesTest.kt new file mode 100644 index 0000000000..6567bd8683 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/types/ScopesTest.kt @@ -0,0 +1,75 @@ +/* + * 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.types + +import com.expediagroup.graphql.generator.federation.directives.Scope +import graphql.GraphQLContext +import graphql.execution.CoercedVariables +import graphql.language.IntValue +import graphql.language.StringValue +import graphql.schema.Coercing +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingSerializeException +import org.junit.jupiter.api.Test +import java.math.BigInteger +import java.util.Locale +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class ScopesTest { + private val coercing: Coercing<*, *> = SCOPE_SCALAR_TYPE.coercing + + @Test + fun `serialize should throw exception when not a Scope`() { + assertFailsWith { + coercing.serialize(StringValue("hello"), GraphQLContext.getDefault(), Locale.ENGLISH) + } + } + + @Test + fun `serialize should return the value from Scope`() { + val result = coercing.serialize(Scope("1"), GraphQLContext.getDefault(), Locale.ENGLISH) + assertEquals(expected = "1", actual = result) + } + + @Test + fun `parseValue should parse StringValue`() { + val result = coercing.parseValue(StringValue("scope"), GraphQLContext.getDefault(), Locale.ENGLISH) + assertTrue(result is Scope) + } + + @Test + fun `parseValue should throw exception on non-StringValue`() { + assertFailsWith { + coercing.parseValue(IntValue(BigInteger.ONE), GraphQLContext.getDefault(), Locale.ENGLISH) + } + } + + @Test + fun `parseLiteral should map StringValue to a Scope`() { + val result = coercing.parseLiteral(StringValue("scope"), CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.ENGLISH) + assertTrue(result is Scope) + } + + @Test + fun `parseLiteral should throw exception on non-StringValue`() { + assertFailsWith { + coercing.parseLiteral(IntValue(BigInteger.ONE), CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.ENGLISH) + } + } +} diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/hooks/SchemaGeneratorHooks.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/hooks/SchemaGeneratorHooks.kt index 682bb6e348..9d71aaf8ec 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/hooks/SchemaGeneratorHooks.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/hooks/SchemaGeneratorHooks.kt @@ -28,6 +28,7 @@ import com.expediagroup.graphql.generator.exceptions.EmptySubscriptionTypeExcept import com.expediagroup.graphql.generator.internal.extensions.isSubclassOf import com.expediagroup.graphql.generator.internal.extensions.isValidAdditionalType import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLDirective import graphql.schema.GraphQLFieldDefinition @@ -79,6 +80,11 @@ interface SchemaGeneratorHooks { */ fun willGenerateDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = null + /** + * Called before transforming directive definition to applied directive. This allows for special handling of the directive transformation (e.g. handling nulls, default parameters, etc). + */ + fun willApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLAppliedDirective? = null + /** * Called after using reflection to generate the graphql object type but before returning it to the schema builder. * This allows for modifying the type info, like description or directives @@ -157,22 +163,27 @@ interface SchemaGeneratorHooks { } /** - * Called after auto-generating the directive from the annotation that allows final transformation before it is applied to a target location. + * Called after auto-generating the directive definition from the annotation that allows final transformation before it is added to the schema document. */ fun didGenerateDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLDirective = directive /** - * Called after converting the function to a field definition but before adding to the query object to allow customization + * Called after transforming directive definition to applied directive that allows for final transformation before it is applied to a target location. + */ + fun didApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLAppliedDirective): GraphQLAppliedDirective = directive + + /** + * Called after converting the function to a field definition but before adding to the query object to allow customization. */ fun didGenerateQueryField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition /** - * Called after converting the function to a field definition but before adding to the mutation object to allow customization + * Called after converting the function to a field definition but before adding to the mutation object to allow customization. */ fun didGenerateMutationField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition /** - * Called after converting the function to a field definition but before adding to the subscription object to allow customization + * Called after converting the function to a field definition but before adding to the subscription object to allow customization. */ fun didGenerateSubscriptionField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt index 03d012cc52..10f4e19530 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt @@ -92,24 +92,26 @@ private fun getDirective(generator: SchemaGenerator, directiveInfo: DirectiveMet generator.config.hooks.didGenerateDirective(directiveInfo, generatedDirective) } - return if (directive.arguments.isNotEmpty()) { - directive.toAppliedDirective() - .transform { builder -> - directiveInfo.directive.annotationClass.getValidProperties(generator.config.hooks).forEach { prop -> - directive.getArgument(prop.name) - ?.toAppliedArgument() - ?.transform { argumentBuilder -> - val value = prop.call(directiveInfo.directive) - argumentBuilder.valueProgrammatic(value) - } - ?.let { appliedDirectiveArgument -> - builder.argument(appliedDirectiveArgument) - } + val appliedDirective = generator.config.hooks.willApplyDirective(directiveInfo, directive) + ?: if (directive.arguments.isNotEmpty()) { + directive.toAppliedDirective() + .transform { builder -> + directiveInfo.directive.annotationClass.getValidProperties(generator.config.hooks).forEach { prop -> + directive.getArgument(prop.name) + ?.toAppliedArgument() + ?.transform { argumentBuilder -> + val value = prop.call(directiveInfo.directive) + argumentBuilder.valueProgrammatic(value) + } + ?.let { appliedDirectiveArgument -> + builder.argument(appliedDirectiveArgument) + } + } } - } - } else { - directive.toAppliedDirective() - } + } else { + directive.toAppliedDirective() + } + return generator.config.hooks.didApplyDirective(directiveInfo, appliedDirective) } private fun generateDirectiveArgument(prop: KProperty<*>, generator: SchemaGenerator): GraphQLArgument { diff --git a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql index 970d9b3f4d..14847a7894 100644 --- a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql +++ b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql @@ -1,4 +1,4 @@ -schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ +schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql index 70a9fb3547..04e17b2e09 100644 --- a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql +++ b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql @@ -1,4 +1,4 @@ -schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ +schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt b/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt index 951691bd31..518b90882f 100755 --- a/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt +++ b/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt @@ -36,7 +36,7 @@ class GenerateSDLMojoTest { assertTrue(schemaFile.exists(), "schema file was generated") val expectedSchema = """ - schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ + schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt b/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt index c596dc2bce..942b48e705 100755 --- a/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt +++ b/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt @@ -36,7 +36,7 @@ class GenerateSDLMojoTest { assertTrue(schemaFile.exists(), "schema file was generated") val expectedSchema = """ - schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ + schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt b/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt index 8071065e52..75837d7a8e 100644 --- a/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt +++ b/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt @@ -25,7 +25,7 @@ class GenerateCustomSDLTest { fun `verify we can generate SDL using custom hooks provider`() { val expectedSchema = """ - schema @link(url : "https://specs.apollo.dev/federation/v2.3"){ + schema @link(url : "https://specs.apollo.dev/federation/v2.5"){ query: Query } diff --git a/website/docs/schema-generator/federation/apollo-federation.mdx b/website/docs/schema-generator/federation/apollo-federation.mdx index ffc5c7aeec..f97979b3c0 100644 --- a/website/docs/schema-generator/federation/apollo-federation.mdx +++ b/website/docs/schema-generator/federation/apollo-federation.mdx @@ -119,22 +119,12 @@ toFederatedSchema( will generate ```graphql -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ +schema @link(import : ["@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){ 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 -directive @interfaceObject on OBJECT directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE directive @link(import: [String], url: String!) repeatable on SCHEMA -directive @override(from: String!) on FIELD_DEFINITION -directive @provides(fields: FieldSet!) on FIELD_DEFINITION -directive @requires(fields: FieldSet!) on FIELD_DEFINITION -directive @shareable on OBJECT | FIELD_DEFINITION -directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION type Query { getUsers: [User!]! diff --git a/website/docs/schema-generator/federation/federated-directives.md b/website/docs/schema-generator/federation/federated-directives.md index d34f13273f..0a7f044b92 100644 --- a/website/docs/schema-generator/federation/federated-directives.md +++ b/website/docs/schema-generator/federation/federated-directives.md @@ -4,7 +4,22 @@ title: Federated Directives --- `graphql-kotlin` supports a number of directives that can be used to annotate a schema and direct certain behaviors. -For more details, see the [Apollo Federation Specification](https://www.apollographql.com/docs/federation/federation-spec/). +For more details, see the [Apollo Federation Specification](https://www.apollographql.com/docs/federation/subgraph-spec/). + +## `@authenticated` directive + +```graphql +directive @authenticated on + ENUM + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | SCALAR +``` + +Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the +[`@requiresScopes`[#requirescope-directive] directive usage. Refer to the [Apollo Router documentation](https://www.apollographql.com/docs/router/configuration/authorization#authenticated) +for additional details. ## `@composeDirective` directive @@ -43,7 +58,7 @@ it will generate following schema schema @composeDirective(name: "@custom") @link(import : ["@custom"], url: "https://myspecs.dev/myCustomDirective/v1.0") -@link(url : "https://specs.apollo.dev/federation/v2.3") +@link(url : "https://specs.apollo.dev/federation/v2.5") { query: Query } @@ -362,7 +377,7 @@ scalar Import The `@link` directive links definitions within the document to external schemas. See [@link specification](https://specs.apollo.dev/link/v1.0) for details. External schemas are identified by their `url`, which ends with a name and version with the following format: `{NAME}/v{MAJOR}.{MINOR}`, -e.g. `url = "https://specs.apollo.dev/federation/v2.3"`. +e.g. `url = "https://specs.apollo.dev/federation/v2.5"`. External types are associated with the target specification by annotating it with `@LinkedSpec` meta annotation. External types defined in the specification will be automatically namespaced (prefixed with `{NAME}__`) unless they are explicitly @@ -577,6 +592,20 @@ type Product @key(fields : "id") { } ``` +## `@requiresScopes` directive + +```graphql +directive @requiresScopes(scopes: [[Scope!]!]!) on + ENUM + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | SCALAR +``` + +Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes. Refer to the +[Apollo Router documentation](https://www.apollographql.com/docs/router/configuration/authorization#requiresscopes) for additional details. + ## `@shareable` directive :::note