From cbac29b6325e75a7e89642489cfdad47acd8c919 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:16:42 -0500 Subject: [PATCH] feat: federation 2.5 support (#347) Adds support for [Federation specification v2.5](https://www.apollographql.com/docs/federation/federation-versions#v25). Changes: * new `@authenticated` directive ```graphql directive @authenticated on ENUM | FIELD_DEFINITION | INTERFACE | OBJECT | SCALAR ``` * new `@requiresScopes` directive ```graphql directive @requiresScopes(scopes: [[Scope!]!]!) on ENUM | FIELD_DEFINITION | INTERFACE | OBJECT | SCALAR scalar Scope ``` --- compatibility/Dockerfile | 2 +- .../main/resources/graphql/schema.graphqls | 2 +- .../federation/graphqljava/Federation.java | 4 +- .../graphqljava/FederationDirectives.java | 20 ++++- .../resources/definitions_fed2_5.graphqls | 83 +++++++++++++++++++ .../graphqljava/FederationTest.java | 10 +++ .../resources/schemas/authorization.graphql | 11 +++ .../schemas/authorization_federated.graphql | 57 +++++++++++++ .../schemas/customAuthenticated.graphql | 13 +++ .../customAuthenticated_federated.graphql | 59 +++++++++++++ 10 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 graphql-java-support/src/main/resources/definitions_fed2_5.graphqls create mode 100644 graphql-java-support/src/test/resources/schemas/authorization.graphql create mode 100644 graphql-java-support/src/test/resources/schemas/authorization_federated.graphql create mode 100644 graphql-java-support/src/test/resources/schemas/customAuthenticated.graphql create mode 100644 graphql-java-support/src/test/resources/schemas/customAuthenticated_federated.graphql diff --git a/compatibility/Dockerfile b/compatibility/Dockerfile index 959e22c5..aa8d0556 100644 --- a/compatibility/Dockerfile +++ b/compatibility/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7.6.0-jdk17 +FROM openjdk:17 EXPOSE 4001 RUN mkdir /app diff --git a/compatibility/src/main/resources/graphql/schema.graphqls b/compatibility/src/main/resources/graphql/schema.graphqls index 34801d7c..73e2fa3e 100644 --- a/compatibility/src/main/resources/graphql/schema.graphqls +++ b/compatibility/src/main/resources/graphql/schema.graphqls @@ -1,6 +1,6 @@ extend schema @link( - url: "https://specs.apollo.dev/federation/v2.3", + url: "https://specs.apollo.dev/federation/v2.5", import: [ "@composeDirective", "@extends", diff --git a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java index 22da66fd..4c3c41fe 100644 --- a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java +++ b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java @@ -27,10 +27,10 @@ public final class Federation { public static final String FEDERATION_SPEC_V2_0 = "https://specs.apollo.dev/federation/v2.0"; public static final String FEDERATION_SPEC_V2_1 = "https://specs.apollo.dev/federation/v2.1"; - public static final String FEDERATION_SPEC_V2_2 = "https://specs.apollo.dev/federation/v2.2"; - public static final String FEDERATION_SPEC_V2_3 = "https://specs.apollo.dev/federation/v2.3"; + public static final String FEDERATION_SPEC_V2_4 = "https://specs.apollo.dev/federation/v2.4"; + public static final String FEDERATION_SPEC_V2_5 = "https://specs.apollo.dev/federation/v2.5"; private static final SchemaGenerator.Options generatorOptions = SchemaGenerator.Options.defaultOptions(); diff --git a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/FederationDirectives.java b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/FederationDirectives.java index c9322d5c..723ef245 100644 --- a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/FederationDirectives.java +++ b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/FederationDirectives.java @@ -4,6 +4,8 @@ import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_1; import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_2; import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_3; +import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_4; +import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_5; import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION; import static graphql.introspection.Introspection.DirectiveLocation.INTERFACE; import static graphql.introspection.Introspection.DirectiveLocation.OBJECT; @@ -15,7 +17,13 @@ import com.apollographql.federation.graphqljava.exceptions.UnsupportedFederationVersionException; import graphql.PublicApi; -import graphql.language.*; +import graphql.language.DirectiveDefinition; +import graphql.language.DirectiveLocation; +import graphql.language.Document; +import graphql.language.InputValueDefinition; +import graphql.language.NonNullType; +import graphql.language.SDLNamedDefinition; +import graphql.language.TypeName; import graphql.parser.Parser; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLDirective; @@ -25,7 +33,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -206,7 +219,10 @@ public static List loadFederationSpecDefinitions(String fede case FEDERATION_SPEC_V2_2: return loadFed2Definitions("definitions_fed2_2.graphqls"); case FEDERATION_SPEC_V2_3: + case FEDERATION_SPEC_V2_4: return loadFed2Definitions("definitions_fed2_3.graphqls"); + case FEDERATION_SPEC_V2_5: + return loadFed2Definitions("definitions_fed2_5.graphqls"); default: throw new UnsupportedFederationVersionException(federationSpec); } diff --git a/graphql-java-support/src/main/resources/definitions_fed2_5.graphqls b/graphql-java-support/src/main/resources/definitions_fed2_5.graphqls new file mode 100644 index 00000000..f924f930 --- /dev/null +++ b/graphql-java-support/src/main/resources/definitions_fed2_5.graphqls @@ -0,0 +1,83 @@ +# +# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql +# + +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @external on OBJECT | FIELD_DEFINITION +directive @extends on OBJECT | INTERFACE +directive @override(from: String!) on FIELD_DEFINITION +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet + +# +# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql +# + +directive @link( + url: String!, + as: String, + import: [Import]) +repeatable on SCHEMA + +scalar Import + +# +# federation-v2.1 +# + +directive @composeDirective(name: String!) repeatable on SCHEMA + +# +# federation-v2.2 +# + +directive @shareable repeatable on FIELD_DEFINITION | OBJECT + +# +# federation-v2.3 +# + +directive @interfaceObject on OBJECT + +# +# federation-v2.5 +# + +directive @authenticated on + ENUM + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | SCALAR + +directive @requiresScopes(scopes: [[Scope!]!]!) on + ENUM + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | SCALAR + +scalar Scope diff --git a/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java b/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java index 2b8d50cb..352ab076 100644 --- a/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java +++ b/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java @@ -299,6 +299,16 @@ public void verifyFederationV2Transformation_nonResolvableKey_doesNotRequireReso FederatedSchemaVerifier.verifyServiceSDL(federatedSchema, expectedFederatedSchemaSDL); } + @Test + public void verifyFederationV2Transformation_authorization() { + verifyFederationTransformation("schemas/authorization.graphql", true); + } + + @Test + public void verifyFederationV2Transformation_customAuthenticated() { + verifyFederationTransformation("schemas/customAuthenticated.graphql", true); + } + private GraphQLSchema verifyFederationTransformation( String schemaFileName, boolean isFederationV2) { final RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build(); diff --git a/graphql-java-support/src/test/resources/schemas/authorization.graphql b/graphql-java-support/src/test/resources/schemas/authorization.graphql new file mode 100644 index 00000000..25ce2bb4 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/authorization.graphql @@ -0,0 +1,11 @@ +extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@authenticated", "@key", "@requiresScopes", "Scope", "FieldSet"]) + +type Product @key(fields: "id") { + id: ID! + name: String! + supplier: String @requiresScopes(scopes: [["scopeA"]]) +} + +type Query { + product(id: ID!): Product @authenticated +} diff --git a/graphql-java-support/src/test/resources/schemas/authorization_federated.graphql b/graphql-java-support/src/test/resources/schemas/authorization_federated.graphql new file mode 100644 index 00000000..d4e4111d --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/authorization_federated.graphql @@ -0,0 +1,57 @@ +schema @link(import : ["@authenticated", "@key", "@requiresScopes", "Scope", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){ + query: Query +} + +directive @authenticated on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @federation__composeDirective(name: String!) repeatable on SCHEMA + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__external on OBJECT | FIELD_DEFINITION + +directive @federation__interfaceObject on OBJECT + +directive @federation__override(from: String!) on FIELD_DEFINITION + +directive @federation__provides(fields: FieldSet!) on FIELD_DEFINITION + +directive @federation__requires(fields: FieldSet!) on FIELD_DEFINITION + +directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION + +directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA + +directive @requiresScopes(scopes: [[Scope!]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +union _Entity = Product + +type Product @key(fields : "id", resolvable : true) { + id: ID! + name: String! + supplier: String @requiresScopes(scopes : [["scopeA"]]) +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID!): Product @authenticated +} + +type _Service { + sdl: String! +} + +scalar FieldSet + +scalar Scope + +scalar _Any + +scalar link__Import diff --git a/graphql-java-support/src/test/resources/schemas/customAuthenticated.graphql b/graphql-java-support/src/test/resources/schemas/customAuthenticated.graphql new file mode 100644 index 00000000..716d5d04 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/customAuthenticated.graphql @@ -0,0 +1,13 @@ +extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"]) + +directive @authenticated(role: [String!]!) on FIELD_DEFINITION + +type Product @key(fields: "id") { + id: ID! + name: String! + supplier: String @authenticated(role: ["manager"]) +} + +type Query { + product(id: ID!): Product +} diff --git a/graphql-java-support/src/test/resources/schemas/customAuthenticated_federated.graphql b/graphql-java-support/src/test/resources/schemas/customAuthenticated_federated.graphql new file mode 100644 index 00000000..6ad49229 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/customAuthenticated_federated.graphql @@ -0,0 +1,59 @@ +schema @link(import : ["@key"], url : "https://specs.apollo.dev/federation/v2.5"){ + query: Query +} + +directive @authenticated(role: [String!]!) on FIELD_DEFINITION + +directive @federation__authenticated on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @federation__composeDirective(name: String!) repeatable on SCHEMA + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__external on OBJECT | FIELD_DEFINITION + +directive @federation__interfaceObject on OBJECT + +directive @federation__override(from: String!) on FIELD_DEFINITION + +directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION + +directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA + +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +union _Entity = Product + +type Product @key(fields : "id", resolvable : true) { + id: ID! + name: String! + supplier: String @authenticated(role : ["manager"]) +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID!): Product +} + +type _Service { + sdl: String! +} + +scalar _Any + +scalar federation__FieldSet + +scalar federation__Scope + +scalar link__Import