diff --git a/examples/federation/compose-router.yaml b/examples/federation/compose-router.yaml deleted file mode 100644 index ebcb2af64f..0000000000 --- a/examples/federation/compose-router.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - router: - image: ghcr.io/apollographql/router:v1.2.1 - volumes: - - ./router.yaml:/dist/config/router.yaml - - ./supergraph.graphql:/dist/config/supergraph.graphql - ports: - - 3000:3000 - - 8088:8088 - command: -c config/router.yaml -s config/supergraph.graphql diff --git a/examples/federation/compose-subgraphs.yaml b/examples/federation/compose-subgraphs.yaml deleted file mode 100644 index 15af3d88b9..0000000000 --- a/examples/federation/compose-subgraphs.yaml +++ /dev/null @@ -1,27 +0,0 @@ -services: - products: - build: - context: . - dockerfile: products-subgraph/Dockerfile - args: - JAR_FILE: ./products-subgraph/build/libs/products-subgraph.jar - ports: - - 8080:8080 - healthcheck: - test: [ "CMD", "curl", "http://products:8080/actuator/health" ] - interval: 5s - timeout: 1s - retries: 10 - reviews: - build: - context: . - dockerfile: reviews-subgraph/Dockerfile - args: - JAR_FILE: ./reviews-subgraph/build/libs/reviews-subgraph.jar - ports: - - 8081:8081 - healthcheck: - test: [ "CMD", "curl", "http://reviews:8081/actuator/health" ] - interval: 5s - timeout: 1s - retries: 10 diff --git a/examples/federation/docker-compose.yaml b/examples/federation/docker-compose.yaml index e8dd0e92d9..e7f4bce474 100644 --- a/examples/federation/docker-compose.yaml +++ b/examples/federation/docker-compose.yaml @@ -1,6 +1,6 @@ services: router: - image: ghcr.io/apollographql/router:v1.2.1 + image: ghcr.io/apollographql/router:v1.10.1 volumes: - ./router.yaml:/dist/config/router.yaml - ./supergraph.graphql:/dist/config/supergraph.graphql 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 1d2d6bccb0..f442e02841 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,18 @@ import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.genera import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDLV2 import com.expediagroup.graphql.generator.annotations.GraphQLName import com.expediagroup.graphql.generator.directives.DirectiveMetaInformation +import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIVE_TYPE import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_TYPE +import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE +import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE_V2 import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_TYPE +import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_NAME +import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_TYPE import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE_V2 @@ -75,7 +80,9 @@ open class FederatedSchemaGeneratorHooks( private val validator = FederatedSchemaValidator() private val federationV2OnlyDirectiveNames: Set = setOf( + COMPOSE_DIRECTIVE_NAME, INACCESSIBLE_DIRECTIVE_NAME, + INTERFACE_OBJECT_DIRECTIVE_NAME, LINK_DIRECTIVE_NAME, OVERRIDE_DIRECTIVE_NAME, SHAREABLE_DIRECTIVE_NAME @@ -91,9 +98,10 @@ open class FederatedSchemaGeneratorHooks( private val federatedDirectiveV2List: List = listOf( COMPOSE_DIRECTIVE_TYPE, EXTENDS_DIRECTIVE_TYPE, - EXTERNAL_DIRECTIVE_TYPE, + EXTERNAL_DIRECTIVE_TYPE_V2, INACCESSIBLE_DIRECTIVE_TYPE, - KEY_DIRECTIVE_TYPE, + INTERFACE_OBJECT_DIRECTIVE_TYPE, + KEY_DIRECTIVE_TYPE_V2, LINK_DIRECTIVE_TYPE, OVERRIDE_DIRECTIVE_TYPE, PROVIDES_DIRECTIVE_TYPE, @@ -117,23 +125,19 @@ open class FederatedSchemaGeneratorHooks( willGenerateFederatedDirective(directiveInfo) } - private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation) = - if (federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName)) { - throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName) - } else if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) { - KEY_DIRECTIVE_TYPE - } else { - super.willGenerateDirective(directiveInfo) - } + private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when { + federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName) -> throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName) + EXTERNAL_DIRECTIVE_NAME == directiveInfo.effectiveName -> EXTERNAL_DIRECTIVE_TYPE + KEY_DIRECTIVE_NAME == directiveInfo.effectiveName -> KEY_DIRECTIVE_TYPE + else -> super.willGenerateDirective(directiveInfo) + } - private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation) = - if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) { - KEY_DIRECTIVE_TYPE_V2 - } else if (LINK_DIRECTIVE_NAME == directiveInfo.effectiveName) { - LINK_DIRECTIVE_TYPE - } else { - super.willGenerateDirective(directiveInfo) - } + private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when (directiveInfo.effectiveName) { + EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2 + KEY_DIRECTIVE_NAME -> KEY_DIRECTIVE_TYPE_V2 + LINK_DIRECTIVE_NAME -> LINK_DIRECTIVE_TYPE + else -> super.willGenerateDirective(directiveInfo) + } override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType { validator.validateGraphQLType(generatedType) diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt index 0da2647281..dd5ed8ed16 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ComposeDirective.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.expediagroup.graphql.generator.federation.directives import com.expediagroup.graphql.generator.annotations.GraphQLDirective @@ -32,7 +48,7 @@ import graphql.schema.GraphQLNonNull * it will generate following schema * * ```graphql - * schema @composeDirective(name: "@myDirective") @link(import : ["composeDirective", "extends", "external", "inaccessible", "key", "override", "provides", "requires", "shareable", "tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1") { + * schema @composeDirective(name: "@myDirective") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ * query: Query * } * 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 bdd3b6859d..587e3c58dd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,11 @@ import graphql.introspection.Introspection.DirectiveLocation /** * ```graphql + * # federation v1 definition * directive @external on FIELD_DEFINITION + * + * # federation v2 definition + * directive @external on OBJECT | FIELD_DEFINITION * ``` * * The @external directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. @external @@ -60,7 +64,7 @@ import graphql.introspection.Introspection.DirectiveLocation @GraphQLDirective( name = EXTERNAL_DIRECTIVE_NAME, description = EXTERNAL_DIRECTIVE_DESCRIPTION, - locations = [DirectiveLocation.FIELD_DEFINITION] + locations = [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION] ) annotation class ExternalDirective @@ -72,3 +76,9 @@ internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql. .description(EXTERNAL_DIRECTIVE_DESCRIPTION) .validLocations(DirectiveLocation.FIELD_DEFINITION) .build() + +internal val EXTERNAL_DIRECTIVE_TYPE_V2: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() + .name(EXTERNAL_DIRECTIVE_NAME) + .description(EXTERNAL_DIRECTIVE_DESCRIPTION) + .validLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION) + .build() diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/InterfaceObjectDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/InterfaceObjectDirective.kt new file mode 100644 index 0000000000..6c1eade691 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/InterfaceObjectDirective.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.generator.federation.directives + +import com.expediagroup.graphql.generator.annotations.GraphQLDirective +import graphql.introspection.Introspection + +/** + * ```graphql + * directive @interfaceObject on OBJECT + * ``` + * + * This directive provides meta information to the router that this entity type defined within this subgraph is an interface in the supergraph. This allows you to extend functionality + * of an interface across the supergraph without having to implement (or even be aware of) all its implementing types. + * + * Example: + * Given an interface that is defined in another subgraph + * + * ```graphql + * interface Product @key(fields: "id") { + * id: ID! + * description: String + * } + * + * type Book implements Product @key(fields: "id") { + * id: ID! + * description: String + * pages: Int! + * } + * + * type Movie implements Product @key(fields: "id") { + * id: ID! + * description: String + * duration: Int! + * } + * ``` + * + * We can extend Product entity in our subgraph and a new field directly to it. This will result in making this new field available to ALL implementing types. + * + * ```kotlin + * @InterfaceObjectDirective + * data class Product(val id: ID) { + * fun reviews(): List = TODO() + * } + * ``` + * + * Which generates the following subgraph schema + * + * ```graphql + * type Product @key(fields: "id") @interfaceObject { + * id: ID! + * reviews: [Review!]! + * } + * ``` + */ +@GraphQLDirective( + name = INTERFACE_OBJECT_DIRECTIVE_NAME, + description = INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION, + locations = [Introspection.DirectiveLocation.OBJECT] +) +annotation class InterfaceObjectDirective + +internal const val INTERFACE_OBJECT_DIRECTIVE_NAME = "interfaceObject" +private const val INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION = "Provides meta information to the router that this entity type is an interface in the supergraph." + +internal val INTERFACE_OBJECT_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() + .name(INTERFACE_OBJECT_DIRECTIVE_NAME) + .description(INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION) + .validLocations(Introspection.DirectiveLocation.OBJECT) + .build() diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt index 841ff99415..e59c7f5cd3 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import graphql.schema.GraphQLList import graphql.schema.GraphQLNonNull const val LINK_SPEC_URL = "https://specs.apollo.dev/link/v1.0/" -const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.1" +const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.3" /** * ```graphql diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ShareableDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ShareableDirective.kt index b482b7e5ec..68c8e6f762 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ShareableDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/ShareableDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import graphql.introspection.Introspection.DirectiveLocation /** * ```graphql - * directive @shareable on FIELD_DEFINITION | OBJECT + * directive @shareable repeatable on FIELD_DEFINITION | OBJECT * ``` * * Shareable directive indicates that given object and/or field can be resolved by multiple subgraphs. If an object is marked as `@shareable` then all its fields are automatically shareable without the @@ -44,6 +44,7 @@ import graphql.introspection.Introspection.DirectiveLocation * } * ``` */ +@Repeatable @GraphQLDirective( name = SHAREABLE_DIRECTIVE_NAME, description = SHAREABLE_DIRECTIVE_DESCRIPTION, @@ -58,4 +59,5 @@ internal val SHAREABLE_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql .name(SHAREABLE_DIRECTIVE_NAME) .description(SHAREABLE_DIRECTIVE_DESCRIPTION) .validLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT) + .repeatable(true) .build() 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 8aa4c2c14b..85fc27db43 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt index 0601539c21..21c5450416 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt @@ -30,7 +30,7 @@ class FederatedSchemaV2GeneratorTest { fun `verify can generate federated schema`() { val expectedSchema = """ - schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ + schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -49,7 +49,7 @@ class FederatedSchemaV2GeneratorTest { directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION + directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -60,6 +60,9 @@ class FederatedSchemaV2GeneratorTest { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + "Provides meta information to the router that this entity type is an interface in the supergraph." + directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE @@ -76,7 +79,7 @@ class FederatedSchemaV2GeneratorTest { directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" - directive @shareable on OBJECT | FIELD_DEFINITION + directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/composeDirective/CustomSchema.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/composeDirective/CustomSchema.kt index ad2aec8e68..0b71d985e5 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/composeDirective/CustomSchema.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/composeDirective/CustomSchema.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.expediagroup.graphql.generator.federation.data.integration.composeDirective import com.expediagroup.graphql.generator.annotations.GraphQLDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/intfObject/IntfObjectQuery.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/intfObject/IntfObjectQuery.kt new file mode 100644 index 0000000000..e871a3ef56 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/intfObject/IntfObjectQuery.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.generator.federation.data.integration.intfObject + +import com.expediagroup.graphql.generator.federation.directives.FieldSet +import com.expediagroup.graphql.generator.federation.directives.InterfaceObjectDirective +import com.expediagroup.graphql.generator.federation.directives.KeyDirective +import com.expediagroup.graphql.generator.scalars.ID + +class IntfObjectQuery { + + fun product(id: ID): Product = TODO() +} + +@InterfaceObjectDirective +@KeyDirective(fields = FieldSet("id")) +data class Product(val id: ID) { + fun reviews(): List = TODO() +} + +data class Review( + val id: String, + val body: String +) 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 2ddc64c8c8..963ed251b0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ scalar CustomScalar""" const val BASE_SERVICE_SDL = """ -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ +schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -97,13 +97,16 @@ directive @composeDirective(name: String!) repeatable on 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 "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +"Provides meta information to the router that this entity type is an interface in the supergraph." +directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" -directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -118,7 +121,7 @@ directive @provides(fields: FieldSet!) on FIELD_DEFINITION directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" -directive @shareable on OBJECT | FIELD_DEFINITION +directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Allows users to annotate fields and types with additional metadata information" directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -145,7 +148,7 @@ scalar FieldSet const val FEDERATED_SERVICE_SDL_V2 = """ -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ +schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -158,11 +161,14 @@ directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFI directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" -directive @external on FIELD_DEFINITION +directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +"Provides meta information to the router that this entity type is an interface in the supergraph." +directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE @@ -179,7 +185,7 @@ directive @provides(fields: FieldSet!) on FIELD_DEFINITION directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" -directive @shareable on OBJECT | FIELD_DEFINITION +directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Allows users to annotate fields and types with additional metadata information" directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/ComposeDirectiveIT.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/ComposeDirectiveIT.kt index 681a1d59e7..646f296ffb 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/ComposeDirectiveIT.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/ComposeDirectiveIT.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.expediagroup.graphql.generator.federation.validation.integration import com.expediagroup.graphql.generator.TopLevelObject @@ -21,7 +37,7 @@ class ComposeDirectiveIT { ) val expected = """ - schema @composeDirective(name : "custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ + schema @composeDirective(name : "custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/InterfaceObjectIT.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/InterfaceObjectIT.kt new file mode 100644 index 0000000000..7263256d77 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/InterfaceObjectIT.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.generator.federation.validation.integration + +import com.expediagroup.graphql.generator.TopLevelObject +import com.expediagroup.graphql.generator.extensions.print +import com.expediagroup.graphql.generator.federation.data.integration.intfObject.IntfObjectQuery +import com.expediagroup.graphql.generator.federation.toFederatedSchema +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class InterfaceObjectIT { + + @Test + fun `verifies applying @composeDirective generates valid schema`() { + val schema = toFederatedSchema( + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.intfObject"), + queries = listOf(TopLevelObject(IntfObjectQuery())) + ) + + val expected = """ + "Provides meta information to the router that this entity type is an interface in the supergraph." + directive @interfaceObject on OBJECT + + union _Entity = Product + + type Product @interfaceObject { + id: ID! + reviews: [Review!]! + } + + 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! + product(id: ID!): Product! + } + + type Review { + body: String! + id: String! + } + + type _Service { + sdl: String! + } + """.trimIndent() + val actual = schema.print( + includeDefaultSchemaDefinition = false, + includeDirectivesFilter = { directive -> "interfaceObject" == directive }, + includeScalarTypes = false + ).trim() + Assertions.assertEquals(expected, actual) + } +} diff --git a/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/CustomSchema.kt b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/CustomSchema.kt new file mode 100644 index 0000000000..a9ea0d6d29 --- /dev/null +++ b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/CustomSchema.kt @@ -0,0 +1,16 @@ +package com.expediagroup.federation.compatibility + +import com.expediagroup.graphql.generator.annotations.GraphQLDirective +import com.expediagroup.graphql.generator.federation.directives.ComposeDirective +import com.expediagroup.graphql.generator.federation.directives.LinkDirective +import com.expediagroup.graphql.server.Schema +import graphql.introspection.Introspection +import org.springframework.stereotype.Component + +@LinkDirective(url = "https://myspecs.dev/myCustomDirective/v1.0", import = ["@custom"]) +@ComposeDirective("@custom") +@Component +class CustomSchema : Schema + +@GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.OBJECT]) +annotation class CustomDirective diff --git a/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Inventory.kt b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Inventory.kt new file mode 100644 index 0000000000..8117298c0c --- /dev/null +++ b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Inventory.kt @@ -0,0 +1,29 @@ +package com.expediagroup.federation.compatibility.model + +import com.expediagroup.graphql.generator.federation.directives.FieldSet +import com.expediagroup.graphql.generator.federation.directives.InterfaceObjectDirective +import com.expediagroup.graphql.generator.federation.directives.KeyDirective +import com.expediagroup.graphql.generator.federation.execution.FederatedTypeSuspendResolver +import com.expediagroup.graphql.generator.scalars.ID +import graphql.schema.DataFetchingEnvironment +import org.springframework.stereotype.Component + +@KeyDirective(fields = FieldSet("id")) +@InterfaceObjectDirective +data class Inventory(val id: ID) { + fun deprecatedProducts(): List = listOf(DEPRECATED_PRODUCT) +} + +@Component +class InventoryResolver : FederatedTypeSuspendResolver { + override val typeName: String = "Inventory" + + override suspend fun resolve( + environment: DataFetchingEnvironment, + representation: Map + ): Inventory? = if (representation["id"] == "apollo-oss") { + Inventory(ID("apollo-oss")) + } else { + null + } +} diff --git a/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Product.kt b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Product.kt index 7dd4f2ccf6..7c3dfd9474 100644 --- a/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Product.kt +++ b/integration/federation-compatibility/src/main/kotlin/com/expediagroup/federation/compatibility/model/Product.kt @@ -1,5 +1,6 @@ package com.expediagroup.federation.compatibility.model +import com.expediagroup.federation.compatibility.CustomDirective import com.expediagroup.graphql.generator.annotations.GraphQLName import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective @@ -50,6 +51,7 @@ type Product @KeyDirective(fields = FieldSet("id")) @KeyDirective(fields = FieldSet("sku package")) @KeyDirective(fields = FieldSet("sku variation { id }")) +@CustomDirective data class Product( val id: ID, val sku: String? = null, diff --git a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql index 4734d1e66d..a5555246a0 100644 --- a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql +++ b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/custom.graphql @@ -1,4 +1,4 @@ -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ +schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -15,7 +15,7 @@ directive @deprecated( directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" -directive @external on FIELD_DEFINITION +directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -26,8 +26,11 @@ directive @include( if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +"Provides meta information to the router that this entity type is an interface in the supergraph." +directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" -directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -42,7 +45,7 @@ directive @provides(fields: FieldSet!) on FIELD_DEFINITION directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" -directive @shareable on OBJECT | FIELD_DEFINITION +directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql index 3f1b0d9f81..ce4a727c30 100644 --- a/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql +++ b/integration/gradle-plugin-integration-tests/src/integration/resources/sdl/federated.graphql @@ -1,4 +1,4 @@ -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ +schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -15,7 +15,7 @@ directive @deprecated( directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" -directive @external on FIELD_DEFINITION +directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -26,8 +26,11 @@ directive @include( if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +"Provides meta information to the router that this entity type is an interface in the supergraph." +directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" -directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -42,7 +45,7 @@ directive @provides(fields: FieldSet!) on FIELD_DEFINITION directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" -directive @shareable on OBJECT | FIELD_DEFINITION +directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt b/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt index 3221c75ffb..24eb368ebe 100755 --- a/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt +++ b/integration/maven-plugin-integration-tests/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt @@ -36,7 +36,7 @@ class GenerateSDLMojoTest { assertTrue(schemaFile.exists(), "schema file was generated") val expectedSchema = """ - schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ + schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -53,7 +53,7 @@ class GenerateSDLMojoTest { directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION + directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -64,8 +64,11 @@ class GenerateSDLMojoTest { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + "Provides meta information to the router that this entity type is an interface in the supergraph." + directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" - directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -80,7 +83,7 @@ class GenerateSDLMojoTest { directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" - directive @shareable on OBJECT | FIELD_DEFINITION + directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt b/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt index bcdabc7f53..0121fa9f58 100755 --- a/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt +++ b/integration/maven-plugin-integration-tests/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt @@ -36,7 +36,7 @@ class GenerateSDLMojoTest { assertTrue(schemaFile.exists(), "schema file was generated") val expectedSchema = """ - schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ + schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -53,7 +53,7 @@ class GenerateSDLMojoTest { directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION + directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -64,8 +64,11 @@ class GenerateSDLMojoTest { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + "Provides meta information to the router that this entity type is an interface in the supergraph." + directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" - directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -80,7 +83,7 @@ class GenerateSDLMojoTest { directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" - directive @shareable on OBJECT | FIELD_DEFINITION + directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt b/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt index 0682f437b4..12eb3835f9 100644 --- a/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt +++ b/plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ class GenerateCustomSDLTest { fun `verify we can generate SDL using custom hooks provider`() { val expectedSchema = """ - schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ + schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -42,7 +42,7 @@ class GenerateCustomSDLTest { directive @extends on OBJECT | INTERFACE "Marks target field as external meaning it will be resolved by federated schema" - directive @external on FIELD_DEFINITION + directive @external on OBJECT | FIELD_DEFINITION "Marks location within schema as inaccessible from the GraphQL Gateway" directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION @@ -53,8 +53,11 @@ class GenerateCustomSDLTest { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + "Provides meta information to the router that this entity type is an interface in the supergraph." + directive @interfaceObject on OBJECT + "Space separated list of primary keys needed to access federated object" - directive @key(fields: FieldSet!) 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(import: [String], url: String!) repeatable on SCHEMA @@ -69,7 +72,7 @@ class GenerateCustomSDLTest { directive @requires(fields: FieldSet!) on FIELD_DEFINITION "Indicates that given object and/or field can be resolved by multiple subgraphs" - directive @shareable on OBJECT | FIELD_DEFINITION + directive @shareable repeatable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if` argument is true." directive @skip( diff --git a/website/docs/schema-generator/federation/apollo-federation.mdx b/website/docs/schema-generator/federation/apollo-federation.mdx index ebf44bb702..0e70c3793d 100644 --- a/website/docs/schema-generator/federation/apollo-federation.mdx +++ b/website/docs/schema-generator/federation/apollo-federation.mdx @@ -119,7 +119,7 @@ toFederatedSchema( will generate ```graphql -schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){ +schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){ query: Query } @@ -127,6 +127,7 @@ directive @composeDirective(name: String!) repeatable on SCHEMA directive @extends on OBJECT | INTERFACE directive @external on FIELD_DEFINITION directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @interfaceObject on OBJECT directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE directive @link(import: [String], url: String!) repeatable on SCHEMA directive @override(from: String!) on FIELD_DEFINITION diff --git a/website/docs/schema-generator/federation/federated-directives.md b/website/docs/schema-generator/federation/federated-directives.md index 530017629e..c2827ec4a9 100644 --- a/website/docs/schema-generator/federation/federated-directives.md +++ b/website/docs/schema-generator/federation/federated-directives.md @@ -35,7 +35,7 @@ it will generate following schema ```graphql schema @composeDirective(name: "@myDirective") -@link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1") +@link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3") { query: Query } @@ -121,7 +121,11 @@ 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 ``` The `@external` directive is used to mark a field as owned by another service. This allows service A to use fields from @@ -211,6 +215,60 @@ type Product { } ``` +## `@interfaceObject` directive + +:::note +Only available in Federation v2. +::: + +```graphql +directive @interfaceObject on OBJECT +``` + +This directive provides meta information to the router that this entity type defined within this subgraph is an interface in the supergraph. This allows you to extend functionality +of an interface across the supergraph without having to implement (or even be aware of) all its implementing types. + +Example: +Given an interface that is defined somewhere in our supergraph + +```graphql +interface Product @key(fields: "id") { + id: ID! + description: String +} + +type Book implements Product @key(fields: "id") { + id: ID! + description: String + pages: Int! +} + +type Movie implements Product @key(fields: "id") { + id: ID! + description: String + duration: Int! +} +``` + +We can extend `Product` entity in our subgraph and a new field directly to it. This will result in making this new field available to ALL implementing types. + +```kotlin +@InterfaceObjectDirective +@KeyDirective(fields = FieldSet("id")) +data class Product(val id: ID) { + fun reviews(): List = TODO() +} +``` + +Which generates the following subgraph schema + +```graphql +type Product @key(fields: "id") @interfaceObject { + id: ID! + reviews: [Review!]! +} +``` + ## `@key` directive ```graphql @@ -470,7 +528,7 @@ Only available in Federation v2. ::: ```graphql -directive @shareable on FIELD_DEFINITION | OBJECT +directive @shareable repeatable on FIELD_DEFINITION | OBJECT ``` Shareable directive indicates that given object and/or field can be resolved by multiple subgraphs. If an object is marked as `@shareable` then all its fields are automatically shareable without the