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 |