From fae9be3b83507f7e3288ae984496d44ba15c340b Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:24:21 -0500 Subject: [PATCH] BREAKING: drop Federation v1 support (#1978) ### :pencil: Description Federation v2 was released almost 3 years ago. It is an evolution of the Federation spec to make it more powerful, flexible and easier to adopt. It is no longer possible to create Federated Supergraphs targeting v1 composition. Users still using fed v1 shoul migrate ASAP to the fed v2. ### :link: Related Issues --------- Co-authored-by: Samuel Vazquez --- .../client/server/CustomFederatedHooks.kt | 2 +- .../build.gradle.kts | 2 +- .../FederatedSchemaGeneratorHooks.kt | 273 ++++++------------ .../directives/ExternalDirective.kt | 10 - .../generator/federation/types/FieldSet.kt | 21 +- .../FederatedSchemaGeneratorTest.kt | 122 ++++---- .../FederatedSchemaV2GeneratorTest.kt | 153 ---------- .../federation/data/TestResolvers.kt | 8 +- .../generator/federation/data/TestSchema.kt | 9 +- .../queries/federated/{v2 => }/Product.kt | 2 +- .../data/queries/federated/v1/Product.kt | 150 ---------- .../execution/EntitiesDataFetcherTest.kt | 8 +- .../execution/ServiceQueryResolverTest.kt | 86 +----- .../FederatedSchemaAutoConfiguration.kt | 1 - .../spring/GraphQLConfigurationProperties.kt | 5 - .../federation/apollo-federation.mdx | 31 +- .../federation/federated-directives.md | 52 ++-- .../federation/federated-schemas.md | 48 +-- .../federation/type-resolution.md | 12 +- .../server/spring-server/spring-properties.md | 1 - 20 files changed, 243 insertions(+), 753 deletions(-) delete mode 100644 generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt rename generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/{v2 => }/Product.kt (99%) delete mode 100644 generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v1/Product.kt diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt index cd90779a3f..5d50e5ff54 100644 --- a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt @@ -11,7 +11,7 @@ import java.util.UUID import kotlin.reflect.KType @Component -class CustomFederatedHooks(resolvers: List) : FederatedSchemaGeneratorHooks(resolvers, true) { +class CustomFederatedHooks(resolvers: List) : FederatedSchemaGeneratorHooks(resolvers,) { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) { UUID::class -> graphqlUUIDType ULocale::class -> graphqlULocaleType diff --git a/generator/graphql-kotlin-federation/build.gradle.kts b/generator/graphql-kotlin-federation/build.gradle.kts index 68b880d9af..648766fe9c 100644 --- a/generator/graphql-kotlin-federation/build.gradle.kts +++ b/generator/graphql-kotlin-federation/build.gradle.kts @@ -27,7 +27,7 @@ tasks { limit { counter = "BRANCH" value = "COVEREDRATIO" - minimum = "0.83".toBigDecimal() + minimum = "0.82".toBigDecimal() } } } 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 9c10766e80..a24222039d 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 @@ -16,7 +16,6 @@ package com.expediagroup.graphql.generator.federation -import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDL import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDLV2 import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.annotations.GraphQLName @@ -25,10 +24,8 @@ import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIV import com.expediagroup.graphql.generator.federation.directives.CONTACT_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.CONTACT_DIRECTIVE_TYPE import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_NAME -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 import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_LATEST_URL import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL_PREFIX @@ -36,7 +33,6 @@ 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.INTERFACE_OBJECT_DIRECTIVE_NAME 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.LINK_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.LINK_SPEC import com.expediagroup.graphql.generator.federation.directives.LinkDirective @@ -45,9 +41,7 @@ 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 -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 @@ -61,7 +55,6 @@ import com.expediagroup.graphql.generator.federation.directives.toAppliedLinkDir 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 import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema import com.expediagroup.graphql.generator.federation.exception.UnknownSpecificationException import com.expediagroup.graphql.generator.federation.execution.EntitiesDataFetcher @@ -70,7 +63,6 @@ import com.expediagroup.graphql.generator.federation.types.ANY_SCALAR_TYPE import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME 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 @@ -91,7 +83,6 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import graphql.schema.GraphQLType -import graphql.schema.SchemaTransformer import java.nio.file.Paths import kotlin.io.path.name import kotlin.reflect.KType @@ -101,44 +92,22 @@ import kotlin.reflect.full.findAnnotation * Hooks for generating federated GraphQL schema. */ open class FederatedSchemaGeneratorHooks( - private val resolvers: List, - private val optInFederationV2: Boolean = true + private val resolvers: List ) : FlowSubscriptionSchemaGeneratorHooks() { private val validator: FederatedSchemaValidator = FederatedSchemaValidator() data class LinkSpec(val namespace: String, val imports: Map) private val linkSpecs: MutableMap = HashMap() - private val federationV2OnlyDirectiveNames: Set = setOf( - COMPOSE_DIRECTIVE_NAME, - INACCESSIBLE_DIRECTIVE_NAME, - INTERFACE_OBJECT_DIRECTIVE_NAME, - LINK_DIRECTIVE_NAME, - OVERRIDE_DIRECTIVE_NAME, - SHAREABLE_DIRECTIVE_NAME - ) - - private val federatedDirectiveV1List: List = listOf( - EXTENDS_DIRECTIVE_TYPE, - EXTERNAL_DIRECTIVE_TYPE, - KEY_DIRECTIVE_TYPE, - PROVIDES_DIRECTIVE_TYPE, - REQUIRES_DIRECTIVE_TYPE - ) - // workaround to https://github.com/ExpediaGroup/graphql-kotlin/issues/1815 // since those scalars can be renamed, we need to ensure we only generate those scalars just once private val fieldSetScalar: GraphQLScalarType by lazy { - if (optInFederationV2) { - FIELD_SET_SCALAR_TYPE.run { - val fieldSetScalarName = namespacedTypeName(FEDERATION_SPEC, this.name) - if (fieldSetScalarName != this.name) { - return@run this.transform { it.name(fieldSetScalarName) } - } else { - this - } + FIELD_SET_SCALAR_TYPE.run { + val fieldSetScalarName = namespacedTypeName(FEDERATION_SPEC, this.name) + if (fieldSetScalarName != this.name) { + return@run this.transform { it.name(fieldSetScalarName) } + } else { + this } - } else { - FIELD_SET_SCALAR_TYPE } } private val linkImportScalar: GraphQLScalarType by lazy { @@ -186,54 +155,52 @@ open class FederatedSchemaGeneratorHooks( additionalInputTypes: Set, schemaObject: TopLevelObject? ): GraphQLSchema.Builder { - if (optInFederationV2) { - // preprocess any @LinkDirective applications to capture namespaces for all the imported specs - val appliedLinkDirectives = schemaObject?.kClass?.annotations?.filterIsInstance(LinkDirective::class.java) - appliedLinkDirectives?.forEach { appliedDirectiveAnnotation -> - val specUrl = Paths.get(appliedDirectiveAnnotation.url) - val spec = specUrl.parent.fileName.name - - if (linkSpecs.containsKey(spec)) { - throw DuplicateSpecificationLinkImport(spec, appliedDirectiveAnnotation.url) - } else { - val nameSpace: String = appliedDirectiveAnnotation.`as`.takeIf { - it.isNotBlank() - } ?: spec - val imports: Map = appliedDirectiveAnnotation.import.associate { import -> - val importedName = import.`as`.takeIf { it.isNotBlank() } ?: import.name - normalizeImportName(import.name) to normalizeImportName(importedName) - } - - val linkSpec = LinkSpec(nameSpace, imports) - linkSpecs[spec] = linkSpec + // preprocess any @LinkDirective applications to capture namespaces for all the imported specs + val appliedLinkDirectives = schemaObject?.kClass?.annotations?.filterIsInstance() + appliedLinkDirectives?.forEach { appliedDirectiveAnnotation -> + val specUrl = Paths.get(appliedDirectiveAnnotation.url) + val spec = specUrl.parent.fileName.name + + if (linkSpecs.containsKey(spec)) { + throw DuplicateSpecificationLinkImport(spec, appliedDirectiveAnnotation.url) + } else { + val nameSpace: String = appliedDirectiveAnnotation.`as`.takeIf { + it.isNotBlank() + } ?: spec + val imports: Map = appliedDirectiveAnnotation.import.associate { import -> + val importedName = import.`as`.takeIf { it.isNotBlank() } ?: import.name + normalizeImportName(import.name) to normalizeImportName(importedName) } - } - // populate defaults - if (!linkSpecs.containsKey(FEDERATION_SPEC)) { - linkSpecs[FEDERATION_SPEC] = LinkSpec( - FEDERATION_SPEC, - listOf( - COMPOSE_DIRECTIVE_NAME, - EXTENDS_DIRECTIVE_NAME, - EXTERNAL_DIRECTIVE_NAME, - INACCESSIBLE_DIRECTIVE_NAME, - INTERFACE_OBJECT_DIRECTIVE_NAME, - KEY_DIRECTIVE_NAME, - OVERRIDE_DIRECTIVE_NAME, - PROVIDES_DIRECTIVE_NAME, - REQUIRES_DIRECTIVE_NAME, - SHAREABLE_DIRECTIVE_NAME, - TAG_DIRECTIVE_NAME, - FIELD_SET_SCALAR_NAME - ).associateWith { it } - ) - } - if (!linkSpecs.containsKey(LINK_SPEC)) { - linkSpecs[LINK_SPEC] = LinkSpec(LINK_SPEC, emptyMap()) + val linkSpec = LinkSpec(nameSpace, imports) + linkSpecs[spec] = linkSpec } } + // populate defaults + if (!linkSpecs.containsKey(FEDERATION_SPEC)) { + linkSpecs[FEDERATION_SPEC] = LinkSpec( + FEDERATION_SPEC, + listOf( + COMPOSE_DIRECTIVE_NAME, + EXTENDS_DIRECTIVE_NAME, + EXTERNAL_DIRECTIVE_NAME, + INACCESSIBLE_DIRECTIVE_NAME, + INTERFACE_OBJECT_DIRECTIVE_NAME, + KEY_DIRECTIVE_NAME, + OVERRIDE_DIRECTIVE_NAME, + PROVIDES_DIRECTIVE_NAME, + REQUIRES_DIRECTIVE_NAME, + SHAREABLE_DIRECTIVE_NAME, + TAG_DIRECTIVE_NAME, + FIELD_SET_SCALAR_NAME + ).associateWith { it } + ) + } + if (!linkSpecs.containsKey(LINK_SPEC)) { + linkSpecs[LINK_SPEC] = LinkSpec(LINK_SPEC, emptyMap()) + } + return super.willBuildSchema(queries, mutations, subscriptions, additionalTypes, additionalInputTypes, schemaObject) } @@ -249,32 +216,18 @@ open class FederatedSchemaGeneratorHooks( } override fun willGenerateDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = - if (optInFederationV2) { - willGenerateFederatedDirectiveV2(directiveInfo) - } else { - willGenerateFederatedDirective(directiveInfo) + when (directiveInfo.effectiveName) { + CONTACT_DIRECTIVE_NAME -> CONTACT_DIRECTIVE_TYPE + EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE + 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) } - private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when { - federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName) -> throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName) - CONTACT_DIRECTIVE_NAME == directiveInfo.effectiveName -> CONTACT_DIRECTIVE_TYPE - 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): GraphQLDirective? = when (directiveInfo.effectiveName) { - CONTACT_DIRECTIVE_NAME -> CONTACT_DIRECTIVE_TYPE - 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 when (directiveInfo.effectiveName) { REQUIRES_SCOPE_DIRECTIVE_NAME -> { @@ -290,18 +243,16 @@ open class FederatedSchemaGeneratorHooks( } override fun didGenerateDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLDirective { - if (optInFederationV2) { - // namespace generated directive if needed - val linkedSpec = directiveInfo.directive.annotationClass.annotations - .filterIsInstance(LinkedSpec::class.java) - .map { it.value } - .firstOrNull() - if (linkedSpec != null) { - val finalName = namespacedTypeName(linkedSpec, directive.name) - if (finalName != directive.name) { - return directive.transform { - it.name(finalName) - } + // namespace generated directive if needed + val linkedSpec = directiveInfo.directive.annotationClass.annotations + .filterIsInstance() + .map { it.value } + .firstOrNull() + if (linkedSpec != null) { + val finalName = namespacedTypeName(linkedSpec, directive.name) + if (finalName != directive.name) { + return directive.transform { + it.name(finalName) } } } @@ -317,37 +268,32 @@ open class FederatedSchemaGeneratorHooks( val originalSchema = builder.build() val originalQuery = originalSchema.queryType - findMissingFederationDirectives(originalSchema.directives).forEach { - builder.additionalDirective(it) + // apply @link federation spec import only if it was not yet specified + val federationSpecImportExists = originalSchema.schemaAppliedDirectives.filter { it.name == "link" }.any { + it.getArgument("url")?.argumentValue?.value?.toString()?.startsWith(FEDERATION_SPEC_URL_PREFIX) == true } - if (optInFederationV2) { - // apply @link federation spec import only if it was not yet specified - val federationSpecImportExists = originalSchema.schemaAppliedDirectives.filter { it.name == "link" }.any { - it.getArgument("url")?.argumentValue?.value?.toString()?.startsWith(FEDERATION_SPEC_URL_PREFIX) == true - } - if (!federationSpecImportExists) { - val fed2Imports = linkSpecs[FEDERATION_SPEC]?.imports - ?.keys - ?.mapNotNull { - val directive = originalSchema.getDirective(it) - if (directive != null) { - return@mapNotNull "@${directive.name}" - } + if (!federationSpecImportExists) { + val fed2Imports = linkSpecs[FEDERATION_SPEC]?.imports + ?.keys + ?.mapNotNull { + val directive = originalSchema.getDirective(it) + if (directive != null) { + return@mapNotNull "@${directive.name}" + } - val scalar = originalSchema.getType(it) as? GraphQLNamedType - if (scalar != null) { - return@mapNotNull scalar.name - } - null + val scalar = originalSchema.getType(it) as? GraphQLNamedType + if (scalar != null) { + return@mapNotNull scalar.name } - ?: emptyList() - val linkDirective = linkDirectiveDefinition(linkImportScalar) - if (!originalSchema.directives.any { it.name == LINK_DIRECTIVE_NAME }) { - // only add @link directive definition if it doesn't exist yet - builder.additionalDirective(linkDirective) + null } - builder.withSchemaAppliedDirective(linkDirective.toAppliedLinkDirective(FEDERATION_SPEC_LATEST_URL, null, fed2Imports)) + ?: emptyList() + val linkDirective = linkDirectiveDefinition(linkImportScalar) + if (!originalSchema.directives.any { it.name == LINK_DIRECTIVE_NAME }) { + // only add @link directive definition if it doesn't exist yet + builder.additionalDirective(linkDirective) } + builder.withSchemaAppliedDirective(linkDirective.toAppliedLinkDirective(FEDERATION_SPEC_LATEST_URL, null, fed2Imports)) } val federatedCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(originalSchema.codeRegistry) @@ -367,28 +313,11 @@ open class FederatedSchemaGeneratorHooks( .additionalType(ANY_SCALAR_TYPE) } - val federatedBuilder = if (optInFederationV2) { - builder - } else { - // transform schema to rename FieldSet to _FieldSet - GraphQLSchema.newSchema(SchemaTransformer.transformSchema(builder.build(), FieldSetTransformer())) - } - // Register the data fetcher for the _service query - val sdl = getFederatedServiceSdl(federatedBuilder.build()) + val sdl = getFederatedServiceSdl(builder.build()) federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, SERVICE_FIELD_DEFINITION.name), DataFetcher { _Service(sdl) }) - return federatedBuilder.codeRegistry(federatedCodeRegistry.build()) - } - - private fun findMissingFederationDirectives(existingDirectives: List): List = if (optInFederationV2) { - emptyList() - } else { - // we auto-add directive definitions only for fed v1 schemas - val existingDirectiveNames = existingDirectives.map { it.name } - federatedDirectiveV1List.filter { - !existingDirectiveNames.contains(it.name) - } + return builder.codeRegistry(federatedCodeRegistry.build()) } /** @@ -399,11 +328,6 @@ open class FederatedSchemaGeneratorHooks( */ override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = GraphQLObjectType.newObject(type) .field(SERVICE_FIELD_DEFINITION) - .also { - if (!optInFederationV2) { - it.withAppliedDirective(EXTENDS_DIRECTIVE_TYPE.toAppliedDirective()) - } - } .build() /** @@ -412,13 +336,7 @@ open class FederatedSchemaGeneratorHooks( * See the federation spec for more details: * https://www.apollographql.com/docs/federation/subgraph-spec/#enhanced-introspection-with-query_service */ - private fun getFederatedServiceSdl(schema: GraphQLSchema): String { - return if (optInFederationV2) { - generateServiceSDLV2(schema) - } else { - generateServiceSDL(schema, false) - } - } + private fun getFederatedServiceSdl(schema: GraphQLSchema): String = generateServiceSDLV2(schema) /** * Get all the federation entities in the _Entity union, aka all the types with the @key directive. @@ -427,11 +345,8 @@ open class FederatedSchemaGeneratorHooks( * https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#union-_entity */ private fun getFederatedEntities(originalSchema: GraphQLSchema): Set { - val keyDirectiveName = if (optInFederationV2) { - namespacedTypeName(FEDERATION_SPEC, KEY_DIRECTIVE_NAME) - } else { - KEY_DIRECTIVE_NAME - } + val keyDirectiveName = namespacedTypeName(FEDERATION_SPEC, KEY_DIRECTIVE_NAME) + val entities = originalSchema.allTypesAsList .filterIsInstance() .filter { type -> type.hasAppliedDirective(keyDirectiveName) } diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ExternalDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ExternalDirective.kt index 66891d83df..a51b83849f 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ExternalDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ExternalDirective.kt @@ -21,10 +21,6 @@ import graphql.introspection.Introspection.DirectiveLocation /** * ```graphql - * # federation v1 definition - * directive @external on FIELD_DEFINITION - * - * # federation v2 definition * directive @external on OBJECT | FIELD_DEFINITION * ``` * @@ -73,12 +69,6 @@ internal const val EXTERNAL_DIRECTIVE_NAME = "external" private const val EXTERNAL_DIRECTIVE_DESCRIPTION = "Marks target field as external meaning it will be resolved by federated schema" internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() - .name(EXTERNAL_DIRECTIVE_NAME) - .description(EXTERNAL_DIRECTIVE_DESCRIPTION) - .validLocations(DirectiveLocation.FIELD_DEFINITION) - .build() - -internal val EXTERNAL_DIRECTIVE_TYPE_V2: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() .name(EXTERNAL_DIRECTIVE_NAME) .description(EXTERNAL_DIRECTIVE_DESCRIPTION) .validLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION) diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/FieldSet.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/FieldSet.kt index c6366bccf5..4a5af0bc54 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/FieldSet.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/FieldSet.kt @@ -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. @@ -29,10 +29,6 @@ import graphql.schema.CoercingSerializeException import graphql.schema.GraphQLArgument import graphql.schema.GraphQLNonNull import graphql.schema.GraphQLScalarType -import graphql.schema.GraphQLSchemaElement -import graphql.schema.GraphQLTypeVisitorStub -import graphql.util.TraversalControl -import graphql.util.TraverserContext import java.util.Locale internal const val FIELD_SET_SCALAR_NAME = "FieldSet" @@ -91,18 +87,3 @@ private object FieldSetCoercing : Coercing { else -> throw CoercingValueToLiteralException(FieldSet::class, input) } } - -/** - * Renames FieldSet scalar (used in Federation V2) to _FieldSet (used in Federation V1). - */ -class FieldSetTransformer : GraphQLTypeVisitorStub() { - override fun visitGraphQLScalarType(node: GraphQLScalarType, context: TraverserContext): TraversalControl { - if (node.name == "FieldSet") { - val legacyFieldSetScalar = node.transform { - it.name("_FieldSet") - } - return changeNode(context, legacyFieldSetScalar) - } - return super.visitGraphQLScalarType(node, context) - } -} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt index 291a02ac97..55fe753ba6 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt @@ -23,6 +23,7 @@ import com.expediagroup.graphql.generator.federation.data.queries.simple.SimpleQ import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME import graphql.schema.GraphQLUnionType +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import kotlin.test.assertNotNull @@ -33,7 +34,7 @@ class FederatedSchemaGeneratorTest { fun `verify can generate federated schema`() { val expectedSchema = """ - schema { + schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){ query: Query } @@ -45,11 +46,8 @@ class FederatedSchemaGeneratorTest { reason: String = "No longer supported" ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION - "Marks target object as extending part of the federated schema" - directive @extends on OBJECT | INTERFACE - "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION + directive @external on OBJECT | FIELD_DEFINITION "Directs the executor to include this field or fragment only when the `if` argument is true" directive @include( @@ -58,16 +56,19 @@ class FederatedSchemaGeneratorTest { ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT "Space separated list of primary keys needed to access federated object" - directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE + directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + + "Links definitions within the document to external schemas." + directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA "Indicates an Input Object is a OneOf Input Object." directive @oneOf on INPUT_OBJECT "Specifies the base type field set that will be selectable by the gateway" - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: FieldSet!) on FIELD_DEFINITION "Specifies required input field set from the base type for a resolver" - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( @@ -81,25 +82,25 @@ class FederatedSchemaGeneratorTest { url: String! ) on SCALAR - interface Product @extends @key(fields : "id") @key(fields : "upc") { - id: String! @external + interface Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) { + id: String! reviews: [Review!]! - upc: String! @external + upc: String! } union _Entity = Author | Book | User - type Author @extends @key(fields : "authorId") { - authorId: Int! @external - name: String! @external + type Author @key(fields : "authorId", resolvable : true) { + authorId: Int! + name: String! } - type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { + type Book implements Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) { author: User! @provides(fields : "name") - id: String! @external + id: String! reviews: [Review!]! shippingCost: String! @requires(fields : "weight") - upc: String! @external + upc: String! weight: Float! @external } @@ -107,7 +108,7 @@ class FederatedSchemaGeneratorTest { value: String! } - type Query @extends { + type Query { "Union of all types that use the @key directive, including both types native to the schema and extended types" _entities(representations: [_Any!]!): [_Entity]! _service: _Service! @@ -120,30 +121,31 @@ class FederatedSchemaGeneratorTest { id: String! } - type User @extends @key(fields : "userId") { - name: String! @external - userId: Int! @external + type User @key(fields : "userId", resolvable : true) { + name: String! + userId: Int! } type _Service { sdl: String! } + "Federation type representing set of fields" + scalar FieldSet + "Federation scalar type used to represent any external entities passed to _entities query." scalar _Any - "Federation type representing set of fields" - scalar _FieldSet + scalar link__Import """.trimIndent() val config = FederatedSchemaGeneratorConfig( - supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v1"), - hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false) + supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated"), + hooks = FederatedSchemaGeneratorHooks(emptyList()) ) val schema = toFederatedSchema(config = config) - - assertEquals(expectedSchema, schema.print().trim()) + Assertions.assertEquals(expectedSchema, schema.print().trim()) val productType = schema.getObjectType("Book") assertNotNull(productType) assertNotNull(productType.hasAppliedDirective(KEY_DIRECTIVE_NAME)) @@ -157,7 +159,7 @@ class FederatedSchemaGeneratorTest { fun `verify generator does not add federation queries for non-federated schemas`() { val expectedSchema = """ - schema { + schema @link(url : "https://specs.apollo.dev/federation/v2.6"){ query: Query } @@ -167,30 +169,18 @@ class FederatedSchemaGeneratorTest { reason: String = "No longer supported" ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION - "Marks target object as extending part of the federated schema" - directive @extends on OBJECT | INTERFACE - - "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION - "Directs the executor to include this field or fragment only when the `if` argument is true" directive @include( "Included when true." if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - "Space separated list of primary keys needed to access federated object" - directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE + "Links definitions within the document to external schemas." + directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA "Indicates an Input Object is a OneOf Input Object." directive @oneOf on INPUT_OBJECT - "Specifies the base type field set that will be selectable by the gateway" - directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - - "Specifies required input field set from the base type for a resolver" - directive @requires(fields: _FieldSet!) on FIELD_DEFINITION - "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( "Skipped when true." @@ -203,7 +193,7 @@ class FederatedSchemaGeneratorTest { url: String! ) on SCALAR - type Query @extends { + type Query { _service: _Service! hello(name: String!): String! } @@ -212,13 +202,12 @@ class FederatedSchemaGeneratorTest { sdl: String! } - "Federation type representing set of fields" - scalar _FieldSet + scalar link__Import """.trimIndent() val config = FederatedSchemaGeneratorConfig( supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.simple"), - hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false) + hooks = FederatedSchemaGeneratorHooks(emptyList()) ) val schema = toFederatedSchema(config, listOf(TopLevelObject(SimpleQuery()))) @@ -226,13 +215,43 @@ class FederatedSchemaGeneratorTest { } @Test - fun `verify a nested federated schema still works`() { + fun `verify a schema with self nested query still works`() { val expectedSchema = """ - schema { + schema @link(url : "https://specs.apollo.dev/federation/v2.6"){ query: Query } + "Marks the field, argument, input field or enum value as deprecated" + directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + + "Directs the executor to include this field or fragment only when the `if` argument is true" + directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + "Links definitions within the document to external schemas." + directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA + + "Indicates an Input Object is a OneOf Input Object." + directive @oneOf on INPUT_OBJECT + + "Directs the executor to skip this field or fragment when the `if` argument is true." + directive @skip( + "Skipped when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + "Exposes a URL that specifies the behaviour of this scalar." + directive @specifiedBy( + "The URL that specifies the behaviour of this scalar." + url: String! + ) on SCALAR + type Query { _service: _Service! getSimpleNestedObject: [SelfReferenceObject]! @@ -248,16 +267,15 @@ class FederatedSchemaGeneratorTest { sdl: String! } - "Federation type representing set of fields" - scalar _FieldSet + scalar link__Import """.trimIndent() val config = FederatedSchemaGeneratorConfig( supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.simple"), - hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false) + hooks = FederatedSchemaGeneratorHooks(emptyList()) ) val schema = toFederatedSchema(config, listOf(TopLevelObject(NestedQuery()))) - assertEquals(expectedSchema, schema.print(includeDirectives = false).trim()) + assertEquals(expectedSchema, schema.print(includeDirectives = true).trim()) } } 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 deleted file mode 100644 index 089b101c68..0000000000 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 - -import com.expediagroup.graphql.generator.extensions.print -import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME -import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME -import graphql.schema.GraphQLUnionType -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class FederatedSchemaV2GeneratorTest { - @Test - fun `verify can generate federated schema`() { - val expectedSchema = - """ - schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){ - query: Query - } - - 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" - directive @deprecated( - "The reason for the deprecation" - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION - - "Marks target field as external meaning it will be resolved by federated schema" - directive @external on OBJECT | FIELD_DEFINITION - - "Directs the executor to include this field or fragment only when the `if` argument is true" - directive @include( - "Included when true." - if: Boolean! - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - - "Space separated list of primary keys needed to access federated object" - directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE - - "Links definitions within the document to external schemas." - directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA - - "Indicates an Input Object is a OneOf Input Object." - directive @oneOf on INPUT_OBJECT - - "Specifies the base type field set that will be selectable by the gateway" - directive @provides(fields: FieldSet!) on FIELD_DEFINITION - - "Specifies required input field set from the base type for a resolver" - directive @requires(fields: FieldSet!) on FIELD_DEFINITION - - "Directs the executor to skip this field or fragment when the `if` argument is true." - directive @skip( - "Skipped when true." - 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 - - interface Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) { - id: String! - reviews: [Review!]! - upc: String! - } - - union _Entity = Author | Book | User - - type Author @key(fields : "authorId", resolvable : true) { - authorId: Int! - name: String! - } - - type Book implements Product @key(fields : "id", resolvable : true) @key(fields : "upc", resolvable : true) { - author: User! @provides(fields : "name") - id: String! - reviews: [Review!]! - shippingCost: String! @requires(fields : "weight") - upc: String! - weight: Float! @external - } - - type CustomScalar { - value: String! - } - - type Query { - "Union of all types that use the @key directive, including both types native to the schema and extended types" - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Review { - body: String! @custom - content: String @deprecated(reason : "no longer supported, replace with use Review.body instead") - customScalar: CustomScalar! - id: String! - } - - type User @key(fields : "userId", resolvable : true) { - name: String! - userId: Int! - } - - type _Service { - sdl: String! - } - - "Federation type representing set of fields" - scalar FieldSet - - "Federation scalar type used to represent any external entities passed to _entities query." - scalar _Any - - scalar link__Import - """.trimIndent() - - val config = FederatedSchemaGeneratorConfig( - supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v2"), - hooks = FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = true) - ) - - val schema = toFederatedSchema(config = config) - Assertions.assertEquals(expectedSchema, schema.print().trim()) - val productType = schema.getObjectType("Book") - assertNotNull(productType) - assertNotNull(productType.hasAppliedDirective(KEY_DIRECTIVE_NAME)) - - val entityUnion = schema.getType(ENTITY_UNION_NAME) as? GraphQLUnionType - assertNotNull(entityUnion) - assertTrue(entityUnion.types.contains(productType)) - } -} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt index 9689b08533..b466328a80 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 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. @@ -16,9 +16,9 @@ package com.expediagroup.graphql.generator.federation.data -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.Author -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.Book -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.User +import com.expediagroup.graphql.generator.federation.data.queries.federated.Author +import com.expediagroup.graphql.generator.federation.data.queries.federated.Book +import com.expediagroup.graphql.generator.federation.data.queries.federated.User import com.expediagroup.graphql.generator.federation.execution.FederatedTypePromiseResolver import com.expediagroup.graphql.generator.federation.execution.FederatedTypeSuspendResolver import graphql.schema.DataFetchingEnvironment diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestSchema.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestSchema.kt index 9b2de4e011..d56a9e459d 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestSchema.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestSchema.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 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. @@ -26,14 +26,9 @@ import graphql.schema.GraphQLSchema internal fun federatedTestSchema( queries: List = emptyList(), federatedTypeResolvers: List> = emptyList(), - isV1: Boolean = true ): GraphQLSchema { val config = FederatedSchemaGeneratorConfig( - supportedPackages = if (isV1) { - listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v1") - } else { - listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v2") - }, + supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated"), hooks = FederatedSchemaGeneratorHooks(federatedTypeResolvers) ) return toFederatedSchema(config = config, queries = queries) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v2/Product.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt similarity index 99% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v2/Product.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt index 1891dbc7ce..79ee2ffe01 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v2/Product.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.queries.federated.v2 +package com.expediagroup.graphql.generator.federation.data.queries.federated import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v1/Product.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v1/Product.kt deleted file mode 100644 index b67618c54a..0000000000 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/v1/Product.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2022 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.queries.federated.v1 - -import com.expediagroup.graphql.generator.annotations.GraphQLDescription -import com.expediagroup.graphql.generator.annotations.GraphQLDirective -import com.expediagroup.graphql.generator.annotations.GraphQLIgnore -import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective -import com.expediagroup.graphql.generator.federation.directives.ExternalDirective -import com.expediagroup.graphql.generator.federation.directives.FieldSet -import com.expediagroup.graphql.generator.federation.directives.KeyDirective -import com.expediagroup.graphql.generator.federation.directives.ProvidesDirective -import com.expediagroup.graphql.generator.federation.directives.RequiresDirective -import kotlin.properties.Delegates - -/* -interface Product @extends @key(fields : "id") @key(fields : "upc") { - id: String! @external - upc: String! @external - reviews: [Review!]! -} - */ -@KeyDirective(fields = FieldSet("id")) -@KeyDirective(fields = FieldSet("upc")) -@ExtendsDirective -interface Product { - @ExternalDirective val id: String - @ExternalDirective val upc: String - fun reviews(): List -} - -/* -type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { - author: User! @provides(fields : "name") - id: String! @external - upc: String! @external - reviews: [Review!]! - shippingCost: String! @requires(fields : "weight") - weight: Float! @external -} - */ -@ExtendsDirective -@KeyDirective(FieldSet("id")) -@KeyDirective(FieldSet("upc")) -class Book( - @ExternalDirective override val id: String, - @ExternalDirective override val upc: String -) : Product { - - constructor(id: String) : this(id, id) - - // optionally provided as it is not part of the @key field set - // will only be specified if federated query attempts to resolve shippingCost - @ExternalDirective - var weight: Double by Delegates.notNull() - - override fun reviews(): List = listOf(Review(id = "parent-$id", body = "Dummy Review $id", content = null, customScalar = CustomScalar("foo"))) - - @RequiresDirective(FieldSet("weight")) - fun shippingCost(): String = "$${weight * 9.99}" - - @ProvidesDirective(FieldSet("name")) - fun author(): User = User(1, "John Doe") - - @Suppress("UnsafeCast") - @GraphQLIgnore - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Book - - if (id != other.id) return false - if (weight != other.weight) return false - - return true - } - - @GraphQLIgnore - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + weight.hashCode() - return result - } -} - -/* -type Review { - body: String! - id: String! -} - */ -data class Review( - val id: String, - @CustomDirective val body: String, - @Deprecated(message = "no longer supported", replaceWith = ReplaceWith("use Review.body instead")) val content: String? = null, - val customScalar: CustomScalar -) - -/* -type User { - age: Int! - name: String! -} - */ -@ExtendsDirective -@KeyDirective(FieldSet("userId")) -data class User( - @ExternalDirective val userId: Int, - @ExternalDirective val name: String -) - -/* -type Author { - authorId: Int! - name: String! -} - */ -@ExtendsDirective -@KeyDirective(FieldSet("authorId")) -data class Author( - @ExternalDirective val authorId: Int, - @ExternalDirective val name: String -) - -@GraphQLDirective(name = "custom") -@GraphQLDescription( - """ - This is a multi-line comment on a custom directive. - This should still work multiline and double quotes (") in the description. - Line 3. - """ -) -annotation class CustomDirective - -class CustomScalar(val value: String) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/EntitiesDataFetcherTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/EntitiesDataFetcherTest.kt index c97387c6de..e1d60ecf19 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/EntitiesDataFetcherTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/EntitiesDataFetcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 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. @@ -19,9 +19,9 @@ package com.expediagroup.graphql.generator.federation.execution import com.expediagroup.graphql.generator.federation.data.AuthorResolver import com.expediagroup.graphql.generator.federation.data.BookResolver import com.expediagroup.graphql.generator.federation.data.UserResolver -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.Author -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.Book -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.User +import com.expediagroup.graphql.generator.federation.data.queries.federated.Author +import com.expediagroup.graphql.generator.federation.data.queries.federated.Book +import com.expediagroup.graphql.generator.federation.data.queries.federated.User import graphql.GraphQLContext import graphql.GraphQLError import graphql.schema.DataFetchingEnvironment 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 9e5d9fbe0a..0a27b698e4 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 @@ -19,7 +19,7 @@ package com.expediagroup.graphql.generator.federation.execution import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks -import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.CustomScalar +import com.expediagroup.graphql.generator.federation.data.queries.federated.CustomScalar import com.expediagroup.graphql.generator.federation.data.queries.simple.NestedQuery import com.expediagroup.graphql.generator.federation.data.queries.simple.SimpleQuery import com.expediagroup.graphql.generator.federation.toFederatedSchema @@ -40,56 +40,6 @@ import kotlin.reflect.KType import kotlin.test.assertEquals import kotlin.test.assertNotNull -// SDL is returned without _entity and _service queries -const val FEDERATED_SERVICE_SDL = -""" -schema { - query: Query -} - -directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - -interface Product @extends @key(fields : "id") @key(fields : "upc") { - id: String! @external - reviews: [Review!]! - upc: String! @external -} - -type Author @extends @key(fields : "authorId") { - authorId: Int! @external - name: String! @external -} - -type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { - author: User! @provides(fields : "name") - id: String! @external - reviews: [Review!]! - shippingCost: String! @requires(fields : "weight") - upc: String! @external - weight: Float! @external -} - -type Query @extends - -type Review { - body: String! @custom - content: String @deprecated(reason : "no longer supported, replace with use Review.body instead") - customScalar: CustomScalar! - id: String! -} - -type User @extends @key(fields : "userId") { - name: String! @external - userId: Int! @external -} - -""${'"'} -This is a multi-line comment on a custom scalar. -This should still work multiline and double quotes (") in the description. -Line 3. -""${'"'} -scalar CustomScalar""" - const val BASE_SERVICE_SDL = """ schema @link(url : "https://specs.apollo.dev/federation/v2.6"){ @@ -200,7 +150,7 @@ scalar link__Import class ServiceQueryResolverTest { - class CustomScalarFederatedHooks : FederatedSchemaGeneratorHooks(emptyList(), optInFederationV2 = false) { + class CustomScalarFederatedHooks : FederatedSchemaGeneratorHooks(emptyList()) { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { CustomScalar::class -> graphqlCustomScalar else -> super.willGenerateGraphQLType(type) @@ -232,36 +182,6 @@ class ServiceQueryResolverTest { } } - @Test - fun `verify can retrieve SDL using _service query`() { - val config = FederatedSchemaGeneratorConfig( - supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v1"), - hooks = CustomScalarFederatedHooks() - ) - - val schema = toFederatedSchema(config = config) - val query = - """ - query sdlQuery { - _service { - sdl - } - } - """.trimIndent() - val executionInput = ExecutionInput.newExecutionInput() - .query(query) - .build() - val graphQL = GraphQL.newGraphQL(schema).build() - val result = graphQL.executeAsync(executionInput).get().toSpecification() - - assertNotNull(result["data"] as? Map<*, *>) { data -> - assertNotNull(data["_service"] as? Map<*, *>) { queryResult -> - val sdl = queryResult["sdl"] as? String - assertEquals(FEDERATED_SERVICE_SDL.trim(), sdl) - } - } - } - @Test fun `verify can retrieve SDL using _service query for non-federated schemas`() { val config = FederatedSchemaGeneratorConfig( @@ -295,7 +215,7 @@ class ServiceQueryResolverTest { @Test fun `verify can retrieve Federation v2 SDL using _service query`() { val config = FederatedSchemaGeneratorConfig( - supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated.v2"), + supportedPackages = listOf("com.expediagroup.graphql.generator.federation.data.queries.federated"), hooks = FederatedSchemaGeneratorHooks(emptyList()) ) diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/FederatedSchemaAutoConfiguration.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/FederatedSchemaAutoConfiguration.kt index bd3bcbc537..ebe305522a 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/FederatedSchemaAutoConfiguration.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/FederatedSchemaAutoConfiguration.kt @@ -61,7 +61,6 @@ class FederatedSchemaAutoConfiguration( resolvers: Optional> ): FederatedSchemaGeneratorHooks = FederatedSchemaGeneratorHooks( resolvers.orElse(emptyList()), - config.federation.optInV2 ) @Bean diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt index 91d467de2c..2ef32396ec 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt @@ -56,11 +56,6 @@ data class GraphQLConfigurationProperties( */ val enabled: Boolean = false, - /** - * Boolean flag indicating whether we want to generate Federation v2 compatible schema. - */ - val optInV2: Boolean = true, - /** * Federation tracing config */ diff --git a/website/docs/schema-generator/federation/apollo-federation.mdx b/website/docs/schema-generator/federation/apollo-federation.mdx index 61a06bae90..bab83f01be 100644 --- a/website/docs/schema-generator/federation/apollo-federation.mdx +++ b/website/docs/schema-generator/federation/apollo-federation.mdx @@ -19,34 +19,6 @@ underlying graph and convey the relationships between different schema types. Ea valid GraphQL schema and can be run independently. This is in contrast with a traditional schema stitching approach where relationships between individual services, i.e. linking configuration, is configured at the GraphQL gateway level. -## Federation v1 vs Federation v2 - -Federation v2 is an evolution of the Federation spec to make it more powerful, flexible and easier to adapt. While v1 and -v2 schemas are similar in many ways, Federation v2 relaxes some of the constraints and adds additional capabilities. See -[Apollo documentation](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) for details. - -By default, `graphql-kotlin-federation` library will generate Federation v2 compatible schema. In order to generate v1 -compatible schema you have to explicitly opt-out by specifying `optInFederationV2 = false` on your instance of -`FederatedSchemaGeneratorHooks`. - -```kotlin -val myHooks = FederatedSchemaGeneratorHooks(resolvers = myFederatedResolvers) -val myConfig = FederatedSchemaGeneratorConfig( - supportedPackages = "com.example", - hooks = myHooks -) - -toFederatedSchema( - config = myConfig, - queries = listOf(TopLevelObject(MyQuery())) -) -``` - -:::note -When generating federated schemas, `graphql-kotlin-spring-server` defaults to Federation v2. If you want to generate Federation -v1 schema, you have to explicitly opt-out by configuring `graphql.federation.optInV2 = false` property. -::: - ## Install Using a JVM dependency manager, link `graphql-kotlin-federation` to your project. @@ -124,7 +96,7 @@ schema @link(import : ["@key", "FieldSet"], url : "https://specs.apollo.dev/fede } directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE -directive @link(import: [String], url: String!) repeatable on SCHEMA +directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA type Query { getUsers: [User!]! @@ -146,4 +118,5 @@ type _Service { scalar FieldSet scalar _Any +scalar link__Import ``` diff --git a/website/docs/schema-generator/federation/federated-directives.md b/website/docs/schema-generator/federation/federated-directives.md index d71fa9dd77..ecf9ecff49 100644 --- a/website/docs/schema-generator/federation/federated-directives.md +++ b/website/docs/schema-generator/federation/federated-directives.md @@ -8,6 +8,10 @@ For more details, see the [Apollo Federation Specification](https://www.apollogr ## `@authenticated` directive +:::info +Available since Federation v2.5 +::: + ```graphql directive @authenticated on ENUM @@ -23,6 +27,10 @@ for additional details. ## `@composeDirective` directive +:::info +Available since Federation v2.1 +::: + ```graphql directive @composeDirective(name: String!) repeatable on SCHEMA ``` @@ -144,10 +152,6 @@ type Product @key(fields : "id") @extends { ## `@external` directive ```graphql -# federation v1 definition -directive @external on FIELD_DEFINITION - -# federation v2 definition directive @external on OBJECT | FIELD_DEFINITION ``` @@ -184,8 +188,8 @@ type Product @key(fields : "id") { ## `@inaccessible` directive -:::note -Only available in Federation v2. +:::info +Available since Federation v2.0 ::: ```graphql @@ -240,8 +244,8 @@ type Product { ## `@interfaceObject` directive -:::note -Only available in Federation v2. +:::info +Available since Federation v2.3 ::: ```graphql @@ -295,10 +299,6 @@ type Product @key(fields: "id") @interfaceObject { ## `@key` directive ```graphql -# federation v1 definition -directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE - -# federation v2 definition directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE ``` @@ -360,8 +360,8 @@ This allows end users to query GraphQL Gateway for any product review fields and ## `@link` directive -:::note -Only available in Federation v2. +:::info +Available since Federation v2.0 ::: :::caution @@ -441,8 +441,8 @@ directive @custom__foo on FIELD_DEFINITION ## `@override` directive -:::note -Only available in Federation v2. +:::info +Available since Federation v2.0 ::: ```graphql @@ -480,6 +480,10 @@ type Product @key(fields: "id") { ## `@policy` directive +:::info +Available since Federation v2.6 +::: + ```graphql directive @policy(policies: [[Policy!]!]!) on ENUM @@ -496,10 +500,6 @@ Directive that is used to indicate that access to the target element is restrict ## `@provides` directive ```graphql -# federation v1 definition -directive @provides(fields: _FieldSet!) on FIELD_DEFINITION - -# federation v2 definition directive @provides(fields: FieldSet!) on FIELD_DEFINITION ``` @@ -565,10 +565,6 @@ In the example above, if user selects `baz` field, it will be resolved locally f ## `@requires` directive ```graphql -# federation v1 definition -directive @requires(fields: _FieldSet!) on FIELD_DEFINITON - -# federation v2 definition directive @requires(fields: FieldSet!) on FIELD_DEFINITON ``` @@ -609,6 +605,10 @@ type Product @key(fields : "id") { ## `@requiresScopes` directive +:::info +Available since Federation v2.5 +::: + ```graphql directive @requiresScopes(scopes: [[Scope!]!]!) on ENUM @@ -623,8 +623,8 @@ Directive that is used to indicate that the target element is accessible only to ## `@shareable` directive -:::note -Only available in Federation v2. +:::info +Available since Federation v2.0 ::: ```graphql diff --git a/website/docs/schema-generator/federation/federated-schemas.md b/website/docs/schema-generator/federation/federated-schemas.md index eecfd48bd2..11c95a4b21 100644 --- a/website/docs/schema-generator/federation/federated-schemas.md +++ b/website/docs/schema-generator/federation/federated-schemas.md @@ -15,20 +15,22 @@ a way to instantiate the underlying federated objects by implementing correspond is then generated by invoking the `toFederatedSchema` function ([link](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/toFederatedSchema.kt#L34)). -**In order to generate valid federated schemas, you will need to annotate both your base schema and the one extending -it**. Federated Gateway (e.g. Apollo) will then combine the individual graphs to form single federated graph. +**In order to generate valid federated schemas, you will need to annotate your entities in all your subgraphs**. +Federated Gateway (e.g. Apollo) will then combine the individual graphs to form single federated graph. :::caution If you are using custom `Query` type then all of you federated GraphQL services have to use the same type. It is not possible for federated services to have different definitions of `Query` type. ::: -### Base Subgraph +### Subgraph A -Base schema defines GraphQL types that will be extended by schemas exposed by other GraphQL services. In the example -below, we define base `Product` type with `id` and `description` fields. `id` is the primary key that uniquely -identifies the `Product` type object and is specified in `@key` directive. Since it is a base schema that doesn't expose -any extended functionality our FederatedTypeRegistry does not include any federated resolvers. +Federation v2 relaxed entity ownership and now every subgraph that defines given entity is its owner. In the example +below, we define `Product` type with `id` and `description` fields. `id` is the primary key that uniquely +identifies the `Product` type object and is specified in `@key` directive. Since it might be possible to resolve +`Product` entity from other subgraphs, we also should specify an "entry point" for the federated type - we need to +create a `FederatedTypeResolver` that will be used to instantiate the federated `Product` type when processing federated +queries. ```kotlin @KeyDirective(fields = FieldSet("id")) @@ -40,8 +42,20 @@ class ProductQuery { } } +// Resolve a "Product" type from the _entities query +class ProductResolver : FederatedTypeSuspendResolver { + override val typeName = "Product" + + override suspend fun resolve( + environment: DataFetchingEnvironment, + representation: Map + ): Product? = + representation["id"]?.toString()?.toIntOrNull()?.let { id -> Product(id) } +} + // Generate the schema -val hooks = FederatedSchemaGeneratorHooks(emptyList()) +val resolvers = listOf(ProductResolver()) +val hooks = FederatedSchemaGeneratorHooks(resolvers) val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks) val queries = listOf(TopLevelObject(ProductQuery())) @@ -73,20 +87,16 @@ type _Service { } ``` -### Extended Subgraph +### Subgraph B -Extended federated GraphQL schemas provide additional functionality to the types already exposed by other GraphQL -services. In the example below, `Product` type is extended to add new `reviews` field to it. Primary key needed to -instantiate the `Product` type (i.e. `id`) has to match the `@key` definition on the base type. Since primary keys are -defined on the base type and are only referenced from the extended type, all of the fields that are part of the field -set specified in `@key` directive have to be marked as `@external`. Finally, we also need to specify an "entry point" -for the federated type - we need to create a FederatedTypeResolver that will be used to instantiate the federated -`Product` type when processing federated queries. +Each subgraph can extend and provide new functionality to entities defined in other subgraphs. In the example below, +`Product` type is extended to add new `reviews` field to it. Primary key needed to instantiate the `Product` type (i.e. `id`) +has to match one of the entity `@key` definitions defined in other subgraphs. Finally, we also need to specify an "entry point" +for the federated type - we need to create a FederatedTypeResolver. ```kotlin @KeyDirective(fields = FieldSet("id")) -@ExtendsDirective -data class Product(@ExternalDirective val id: Int) { +data class Product(val id: Int) { // Add the "reviews" field to the type suspend fun reviews(): List = getReviewByProductId(id) } @@ -143,7 +153,7 @@ type _Service { ### Federated Supergraph -Once we have both base and extended GraphQL services up and running, we will also need to configure Federated Gateway +Once we have both GraphQL subgraphs up and running, we will also need to configure Federated Gateway to combine them into a single supergraph schema. Using the examples above, our final federated schema will be generated as: ```graphql diff --git a/website/docs/schema-generator/federation/type-resolution.md b/website/docs/schema-generator/federation/type-resolution.md index 347a233147..3d68a2a3a4 100644 --- a/website/docs/schema-generator/federation/type-resolution.md +++ b/website/docs/schema-generator/federation/type-resolution.md @@ -51,10 +51,9 @@ the suspending function on a `CoroutineScope` to **asynchronously wait** to comp requested in the `_entities` query. ```kotlin -// This service does not own the "Product" type but is extending it with new fields +// This service extends "Product" type with new fields @KeyDirective(fields = FieldSet("id")) -@ExtendsDirective -class Product(@ExternalDirective val id: String) { +class Product(val id: String) { fun newField(): String = getNewFieldByProductId(id) } @@ -80,7 +79,7 @@ class ProductResolver : FederatedTypeSuspendResolver { :::note this suspend implementation relies on the same coroutine scope propagation as the default `FunctionDataFetcher`. See [asynchronous models documentation](../execution/async-models.md) for additional details. -Additionally you can also use `FederatedTypePromiseResolver` which is compatible with `DataLoader`'s async model given that returns +Additionally, you can also use `FederatedTypePromiseResolver` which is compatible with `DataLoader`'s async model given that returns a `CompletableFuture`, that way you get advantage of batching and deduplication of transactions to downstream. ::: @@ -90,10 +89,9 @@ a `CompletableFuture`, that way you get advantage of batching and deduplication a nullable instance of target entity. ```kotlin -// This service does not own the "Product" type but is extending it with new fields +// This service extends "Product" type with new fields @KeyDirective(fields = FieldSet("id")) -@ExtendsDirective -class Product(@ExternalDirective val id: String) { +class Product(val id: String) { fun newField(): String = getNewFieldByProductId(id) } diff --git a/website/docs/server/spring-server/spring-properties.md b/website/docs/server/spring-server/spring-properties.md index 717e9f3ffa..f79f48fb9d 100644 --- a/website/docs/server/spring-server/spring-properties.md +++ b/website/docs/server/spring-server/spring-properties.md @@ -16,7 +16,6 @@ details on the supported configuration properties. | graphql.packages | List of supported packages that can contain GraphQL schema type definitions | | | graphql.printSchema | Boolean flag indicating whether to print the schema after generator creates it | false | | graphql.federation.enabled | Boolean flag indicating whether to generate federated GraphQL model | false | -| graphql.federation.optInV2 | Boolean flag indicating whether to generate Federation v2 GraphQL model | false | | graphql.federation.tracing.enabled | Boolean flag indicating whether add federated tracing data to the extensions | true (if federation enabled) | | graphql.federation.tracing.debug | Boolean flag to log debug info in the federated tracing | false (if federation enabled) | | graphql.introspection.enabled | Boolean flag indicating whether introspection queries are enabled | true |