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 e1811e6f..eeea028f 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 @@ -35,6 +35,7 @@ public final class Federation { public static final String FEDERATION_SPEC_V2_6 = "https://specs.apollo.dev/federation/v2.6"; public static final String FEDERATION_SPEC_V2_7 = "https://specs.apollo.dev/federation/v2.7"; public static final String FEDERATION_SPEC_V2_8 = "https://specs.apollo.dev/federation/v2.8"; + public static final String FEDERATION_SPEC_V2_9 = "https://specs.apollo.dev/federation/v2.9"; private static final SchemaGenerator.Options defaultGeneratorOptions = 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 254ea2e3..8898c943 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 @@ -9,6 +9,7 @@ import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_6; import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_7; import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_8; +import static com.apollographql.federation.graphqljava.Federation.FEDERATION_SPEC_V2_9; import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION; import static graphql.introspection.Introspection.DirectiveLocation.INTERFACE; import static graphql.introspection.Introspection.DirectiveLocation.OBJECT; @@ -232,6 +233,8 @@ public static List loadFederationSpecDefinitions(String fede return loadFed2Definitions("definitions_fed2_7.graphqls"); case FEDERATION_SPEC_V2_8: return loadFed2Definitions("definitions_fed2_8.graphqls"); + case FEDERATION_SPEC_V2_9: + return loadFed2Definitions("definitions_fed2_9.graphqls"); default: throw new UnsupportedFederationVersionException(federationSpec); } diff --git a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/directives/LinkDirectiveProcessor.java b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/directives/LinkDirectiveProcessor.java index 646500a9..9fe1fae2 100644 --- a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/directives/LinkDirectiveProcessor.java +++ b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/directives/LinkDirectiveProcessor.java @@ -32,7 +32,11 @@ public final class LinkDirectiveProcessor { "@interfaceObject", 23, "@authenticated", 25, "@requiresScopes", 25, - "@policy", 26); + "@policy", 26, + "@context", 28, + "@fromContext", 28, + "@cost", 29, + "@listSize", 29); private LinkDirectiveProcessor() {} diff --git a/graphql-java-support/src/main/resources/definitions_fed2_9.graphqls b/graphql-java-support/src/main/resources/definitions_fed2_9.graphqls new file mode 100644 index 00000000..356988c7 --- /dev/null +++ b/graphql-java-support/src/main/resources/definitions_fed2_9.graphqls @@ -0,0 +1,136 @@ +# +# 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 @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], + for: Purpose) +repeatable on SCHEMA + +scalar Import + +enum Purpose { + SECURITY + EXECUTION +} + +# +# 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 + +# +# federation-v2.6 +# + +directive @policy(policies: [[Policy!]!]!) on + ENUM + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | SCALAR + +scalar Policy + +# +# federation-v2.7 +# + +directive @override(from: String!, label: String) on FIELD_DEFINITION + +# +# federation-v2.8 +# + +scalar ContextFieldValue + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @fromContext(field: ContextFieldValue) on ARGUMENT_DEFINITION + +# +# federation-v2.9 +# + +directive @cost(weight: Int!) on + ARGUMENT_DEFINITION + | ENUM + | FIELD_DEFINITION + | INPUT_FIELD_DEFINITION + | OBJECT + | SCALAR + +directive @listSize( + assumedSize: Int, + slicingArguments: [String!], + sizedFields: [String!], + requireOneSlicingArgument: Boolean = true +) on FIELD_DEFINITION \ No newline at end of file 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 8e23601b..81fc0559 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 @@ -332,6 +332,14 @@ public void verifyFederationV2Transformation_context() { verifyFederationTransformation("schemas/context/schema.graphql"); } + @Test + public void verifyFederationV2Transformation_contextFromUnsupportedVersion_throwsException() { + final String schemaSDL = FileUtils.readResource("schemas/invalidSpecVersionContext.graphql"); + assertThrows( + UnsupportedLinkImportException.class, + () -> Federation.transform(schemaSDL).fetchEntities(env -> null).build()); + } + @Test public void verifyFederationV2Transformation_requiresScopesFromUnsupportedVersion_throwsException() { @@ -370,6 +378,19 @@ public void verifyFederationV2Transformation_scalarsDefinedInSchemaButNotWired() verifyFederationTransformation("schemas/scalars/schema.graphql"); } + @Test + public void verifyFederationV2Transformation_cost() { + verifyFederationTransformation("schemas/cost/schema.graphql"); + } + + @Test + public void verifyFederationV2Transformation_costFromUnsupportedVersion_throwsException() { + final String schemaSDL = FileUtils.readResource("schemas/invalidSpecVersionCost.graphql"); + assertThrows( + UnsupportedLinkImportException.class, + () -> Federation.transform(schemaSDL).fetchEntities(env -> null).build()); + } + private void verifyFederationTransformation(String schemaFileName) { final RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build(); verifyFederationTransformation(schemaFileName, runtimeWiring); diff --git a/graphql-java-support/src/test/resources/schemas/cost/schema.graphql b/graphql-java-support/src/test/resources/schemas/cost/schema.graphql new file mode 100644 index 00000000..c60b9393 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/cost/schema.graphql @@ -0,0 +1,20 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.9" + import: ["@cost", "@key", "@listSize"] + ) + +type Product @key(fields: "id") { + id: ID! + name: String! + child: Child! @cost(weight: 5) +} + +type Child @key(fields: "id") { + id: ID! + details(productName: String): [String!] @listSize(assumedSize: 10) +} + +type Query { + product(id: ID!): Product +} diff --git a/graphql-java-support/src/test/resources/schemas/cost/schema_federated.graphql b/graphql-java-support/src/test/resources/schemas/cost/schema_federated.graphql new file mode 100644 index 00000000..849024f7 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/cost/schema_federated.graphql @@ -0,0 +1,81 @@ +schema @link(import : ["@cost", "@key", "@listSize"], url : "https://specs.apollo.dev/federation/v2.9"){ + query: Query +} + +directive @cost(weight: Int!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +directive @federation__authenticated on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @federation__composeDirective(name: String!) repeatable on SCHEMA + +directive @federation__context(name: String!) repeatable on OBJECT | INTERFACE | UNION + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__external on OBJECT | FIELD_DEFINITION + +directive @federation__fromContext(field: federation__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @federation__interfaceObject on OBJECT + +directive @federation__override(from: String!, label: String) on FIELD_DEFINITION + +directive @federation__policy(policies: [[federation__Policy!]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +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, for: link__Purpose, import: [link__Import], url: String!) repeatable on SCHEMA + +directive @listSize(assumedSize: Int, requireOneSlicingArgument: Boolean = true, sizedFields: [String!], slicingArguments: [String!]) on FIELD_DEFINITION + +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +union _Entity = Child | Product + +type Child @key(fields : "id", resolvable : true) { + details(productName: String): [String!] @listSize(assumedSize : 10, requireOneSlicingArgument : true) + id: ID! +} + +type Product @key(fields : "id", resolvable : true) { + child: Child! @cost(weight : 5) + id: ID! + name: String! +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID!): Product +} + +type _Service { + sdl: String! +} + +enum link__Purpose { + EXECUTION + SECURITY +} + +scalar _Any + +scalar federation__ContextFieldValue + +scalar federation__FieldSet + +scalar federation__Policy + +scalar federation__Scope + +scalar link__Import \ No newline at end of file diff --git a/graphql-java-support/src/test/resources/schemas/cost/schema_full.graphql b/graphql-java-support/src/test/resources/schemas/cost/schema_full.graphql new file mode 100644 index 00000000..81901068 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/cost/schema_full.graphql @@ -0,0 +1,108 @@ +schema @link(import : ["@cost", "@key", "@listSize"], url : "https://specs.apollo.dev/federation/v2.9"){ + query: Query +} + +directive @cost(weight: Int!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | 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 + +directive @federation__authenticated on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +directive @federation__composeDirective(name: String!) repeatable on SCHEMA + +directive @federation__context(name: String!) repeatable on OBJECT | INTERFACE | UNION + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__external on OBJECT | FIELD_DEFINITION + +directive @federation__fromContext(field: federation__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @federation__interfaceObject on OBJECT + +directive @federation__override(from: String!, label: String) on FIELD_DEFINITION + +directive @federation__policy(policies: [[federation__Policy!]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM + +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 + +"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 + +directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @link(as: String, for: link__Purpose, import: [link__Import], url: String!) repeatable on SCHEMA + +directive @listSize(assumedSize: Int, requireOneSlicingArgument: Boolean = true, sizedFields: [String!], slicingArguments: [String!]) on FIELD_DEFINITION + +"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 + +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +union _Entity = Child | Product + +type Child @key(fields : "id", resolvable : true) { + details(productName: String): [String!] @listSize(assumedSize : 10, requireOneSlicingArgument : true) + id: ID! +} + +type Product @key(fields : "id", resolvable : true) { + child: Child! @cost(weight : 5) + id: ID! + name: String! +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + product(id: ID!): Product +} + +type _Service { + sdl: String! +} + +enum link__Purpose { + EXECUTION + SECURITY +} + +scalar _Any + +scalar federation__ContextFieldValue + +scalar federation__FieldSet + +scalar federation__Policy + +scalar federation__Scope + +scalar link__Import \ No newline at end of file diff --git a/graphql-java-support/src/test/resources/schemas/invalidSpecVersionContext.graphql b/graphql-java-support/src/test/resources/schemas/invalidSpecVersionContext.graphql new file mode 100644 index 00000000..8e413c2d --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/invalidSpecVersionContext.graphql @@ -0,0 +1,20 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.7" + import: ["@key", "@context", "@fromContext"] + ) + +type Product @key(fields: "id") @context(name: "product") { + id: ID! + name: String! + child: Child! +} + +type Child @key(fields: "id") { + id: ID! + details(productName: String @fromContext(field: "$product { name }")): String! +} + +type Query { + product(id: ID!): Product +} diff --git a/graphql-java-support/src/test/resources/schemas/invalidSpecVersionCost.graphql b/graphql-java-support/src/test/resources/schemas/invalidSpecVersionCost.graphql new file mode 100644 index 00000000..ac9727d8 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/invalidSpecVersionCost.graphql @@ -0,0 +1,20 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.8" + import: ["@cost", "@key", "@listSize"] + ) + +type Product @key(fields: "id") { + id: ID! + name: String! + child: Child! @cost(weight: 5) +} + +type Child @key(fields: "id") { + id: ID! + details(productName: String): [String!] @listSize(assumedSize: 10) +} + +type Query { + product(id: ID!): Product +}