diff --git a/README.adoc b/README.adoc
index 2f4238047..8ea2ae909 100644
--- a/README.adoc
+++ b/README.adoc
@@ -2,6 +2,7 @@
:graphql-over-http: https://github.com/graphql/graphql-over-http
:subscriptions-transport-ws: https://github.com/apollographql/subscriptions-transport-ws
:graphql-ws: https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
+:graphql-federation: https://www.apollographql.com/docs/federation
image:https://github.com/smallrye/smallrye-graphql/workflows/SmallRye%20Build/badge.svg?branch=main[link=https://github.com/smallrye/smallrye-graphql/actions?query=workflow%3A%22SmallRye+Build%22]
image:https://sonarcloud.io/api/project_badges/measure?project=smallrye_smallrye-graphql&metric=alert_status["Quality Gate Status", link="https://sonarcloud.io/dashboard?id=smallrye_smallrye-graphql"]
@@ -16,6 +17,7 @@ SmallRye GraphQL is an implementation of
- {graphql-over-http}[GraphQL over HTTP].
- {graphql-ws}[GraphQL over WebSocket].
- {subscriptions-transport-ws}[Subscriptions transport ws] (old).
+- {graphql-federation}[GraphQL Federation]
== Instructions
diff --git a/docs/federation.md b/docs/federation.md
new file mode 100644
index 000000000..e7ee83088
--- /dev/null
+++ b/docs/federation.md
@@ -0,0 +1,60 @@
+# Federation
+
+To enable support for [GraphQL Federation](https://www.apollographql.com/docs/federation), simply set the `smallrye.graphql.federation.enabled` config key to `true`.
+
+You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class:
+
+```java
+package org.example.price;
+
+import org.eclipse.microprofile.graphql.Id;
+
+import io.smallrye.graphql.api.federation.Extends;
+import io.smallrye.graphql.api.federation.Key;
+
+@Extends @Key(fields = "id")
+public class Product {
+ @Id
+ private String id;
+
+ @Description("The price in cent")
+ private Integer price;
+
+ // getters and setters omitted
+}
+```
+
+And a normal query method that takes the `key` fields as a parameter and returns the requested type:
+
+```java
+package org.example.price;
+
+import org.eclipse.microprofile.graphql.GraphQLApi;
+import org.eclipse.microprofile.graphql.Id;
+import org.eclipse.microprofile.graphql.Query;
+
+@GraphQLApi
+public class Prices {
+ @Query
+ public Product product(@Id String id) {
+ return ...;
+ }
+}
+```
+
+The GraphQL Schema then contains:
+
+```graphql
+type Product @extends @key(fields : ["id"]) {
+ id: ID
+ price: Int
+}
+
+union _Entity = Product
+
+type Query {
+ _entities(representations: [_Any!]!): [_Entity]!
+ _service: _Service!
+ product(id: ID): Product
+}
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 80d548a06..926432bda 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,6 +7,7 @@ nav:
- Server side features:
- Customizing JSON deserializers: 'custom-json-deserializers.md'
- Directives: 'directives.md'
+ - Federation: 'federation.md'
- Custom error extensions: 'custom-error-extensions.md'
- Typesafe client:
- Basic usage: 'typesafe-client-usage.md'
diff --git a/pom.xml b/pom.xml
index 3db7dffe5..53c43c748 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,7 @@
2.1.1
5.0.0
2.0.0
+ 2.0.8
19.2
1.9.3
4.3.3
@@ -247,6 +248,11 @@
graphql-java
${version.graphql-java}
+
+ com.apollographql.federation
+ federation-graphql-java-support
+ ${version.graphql-java-federation}
+
io.vertx
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Extends.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java
similarity index 95%
rename from server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Extends.java
rename to server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java
index d17fd1648..2220567f0 100644
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Extends.java
+++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java
@@ -1,4 +1,4 @@
-package io.smallrye.graphql.federation.api;
+package io.smallrye.graphql.api.federation;
import static io.smallrye.graphql.api.DirectiveLocation.INTERFACE;
import static io.smallrye.graphql.api.DirectiveLocation.OBJECT;
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/External.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java
similarity index 94%
rename from server/federation/api/src/main/java/io/smallrye/graphql/federation/api/External.java
rename to server/api/src/main/java/io/smallrye/graphql/api/federation/External.java
index ec5d81e74..431a906cd 100644
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/External.java
+++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java
@@ -1,4 +1,4 @@
-package io.smallrye.graphql.federation.api;
+package io.smallrye.graphql.api.federation;
import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Key.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java
similarity index 95%
rename from server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Key.java
rename to server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java
index 908e7a979..6ae31a6df 100644
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Key.java
+++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java
@@ -1,4 +1,4 @@
-package io.smallrye.graphql.federation.api;
+package io.smallrye.graphql.api.federation;
import static io.smallrye.graphql.api.DirectiveLocation.INTERFACE;
import static io.smallrye.graphql.api.DirectiveLocation.OBJECT;
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Provides.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java
similarity index 95%
rename from server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Provides.java
rename to server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java
index eae1baf23..df63a6d2d 100644
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Provides.java
+++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java
@@ -1,4 +1,4 @@
-package io.smallrye.graphql.federation.api;
+package io.smallrye.graphql.api.federation;
import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Requires.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java
similarity index 95%
rename from server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Requires.java
rename to server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java
index 1f4c39461..90cce5924 100644
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/Requires.java
+++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java
@@ -1,4 +1,4 @@
-package io.smallrye.graphql.federation.api;
+package io.smallrye.graphql.api.federation;
import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/server/federation/README.adoc b/server/federation/README.adoc
deleted file mode 100644
index 5efb61ba1..000000000
--- a/server/federation/README.adoc
+++ /dev/null
@@ -1,46 +0,0 @@
-= GraphQL Federation
-
-[NOTE]
-The current status is experimental!
-
-Extension library to add support for https://www.apollographql.com/docs/federation/federation-spec/[GraphQL Federation].
-
-The `api` module defines the directives `@Key`, etc. The `runtime` module hooks into the SmallRye GraphQL events to dynamically enhance the GraphQL schema with the Federation extras (`_Entity`, `_Service`, etc.).
-
-== API
-
-Annotate the type you want to extend with `@Key`, e.g.:
-
-[source,java]
-----------
-@Key(fields = "id")
-public class Film {
- String id;
- String year;
- String name;
- // ...
-}
-----------
-
-In the service that extends that type, declare a reduced view that only defines the keys, e.g. `id`. Annotate the type as `@Extends` and the field as `@External`, e.g.:
-
-[source,java]
-----------
-@Extends @Key(fields = "id")
-public class Film {
- @External String id;
-}
-----------
-
-And write a resolver method that adds the fields to that type, just like a 'local' `@Source` resolver would, but use `@FederatedSource` instead, e.g.:
-
-[source,java]
-----------
-class Reviews {
- public List reviews(@FederatedSource Film film) {
- //...
- }
-}
-----------
-
-For a full example, see https://github.com/t1/graphql-federation-demo
diff --git a/server/federation/api/pom.xml b/server/federation/api/pom.xml
deleted file mode 100644
index 07b68ca56..000000000
--- a/server/federation/api/pom.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
- 4.0.0
-
-
- io.smallrye
- smallrye-graphql-federation-parent
- 2.0.0-SNAPSHOT
- ../pom.xml
-
-
- smallrye-graphql-federation-api
- SmallRye: GraphQL Federation :: API
-
-
-
- org.eclipse.microprofile.graphql
- microprofile-graphql-api
- provided
-
-
- io.smallrye
- smallrye-graphql-api
- provided
-
-
-
diff --git a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/FederatedSource.java b/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/FederatedSource.java
deleted file mode 100644
index b47fc767e..000000000
--- a/server/federation/api/src/main/java/io/smallrye/graphql/federation/api/FederatedSource.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.smallrye.graphql.federation.api;
-
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import io.smallrye.common.annotation.Experimental;
-
-/**
- * A federated resolver method is the federated equivalent to a 'local' resolver method (with a parameter annotated as
- * {@link org.eclipse.microprofile.graphql.Source @Source}
), i.e. it adds a field with the name of the method
- * and the type of the method return type to the source object.
- * The class of the source parameter must be annotated as {@link Extends @Extends}
- * and have at least one field annotated as {@link External @External}
.
- */
-@Retention(RUNTIME)
-@Target(PARAMETER)
-@Experimental("SmallRye GraphQL Federation is still subject to change.")
-public @interface FederatedSource {
-}
diff --git a/server/federation/pom.xml b/server/federation/pom.xml
deleted file mode 100644
index dcef61c7f..000000000
--- a/server/federation/pom.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
- 4.0.0
-
-
- io.smallrye
- smallrye-graphql-server-parent
- 2.0.0-SNAPSHOT
- ../pom.xml
-
-
- smallrye-graphql-federation-parent
- SmallRye: GraphQL Federation
- pom
-
-
- api
- runtime
-
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.2.1
-
-
- attach-sources
-
- jar-no-fork
-
-
-
-
-
-
-
diff --git a/server/federation/runtime/pom.xml b/server/federation/runtime/pom.xml
deleted file mode 100644
index c7a0425fd..000000000
--- a/server/federation/runtime/pom.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
- 4.0.0
-
-
- io.smallrye
- smallrye-graphql-federation-parent
- 2.0.0-SNAPSHOT
- ../pom.xml
-
-
- smallrye-graphql-federation-runtime
- SmallRye: GraphQL Federation :: Runtime
-
-
-
- io.smallrye
- smallrye-graphql-federation-api
- ${project.version}
-
-
-
- jakarta.enterprise
- jakarta.enterprise.cdi-api
- provided
-
-
- io.smallrye
- smallrye-graphql
- provided
-
-
- io.smallrye
- smallrye-graphql-schema-builder
- provided
-
-
- org.jboss.logging
- jboss-logging
- provided
-
-
-
diff --git a/server/federation/runtime/src/main/java/io/smallrye/graphql/federation/impl/Federation.java b/server/federation/runtime/src/main/java/io/smallrye/graphql/federation/impl/Federation.java
deleted file mode 100644
index 200e7abda..000000000
--- a/server/federation/runtime/src/main/java/io/smallrye/graphql/federation/impl/Federation.java
+++ /dev/null
@@ -1,444 +0,0 @@
-package io.smallrye.graphql.federation.impl;
-
-import static graphql.Scalars.GraphQLString;
-import static graphql.schema.GraphQLArgument.newArgument;
-import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
-import static graphql.schema.GraphQLList.list;
-import static graphql.schema.GraphQLNonNull.nonNull;
-import static java.util.stream.Collectors.toList;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Scanner;
-import java.util.function.Function;
-
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.enterprise.event.Observes;
-import jakarta.enterprise.inject.spi.CDI;
-import jakarta.inject.Inject;
-
-import org.eclipse.microprofile.graphql.GraphQLApi;
-import org.jboss.jandex.AnnotationInstance;
-import org.jboss.jandex.AnnotationTarget;
-import org.jboss.jandex.ClassInfo;
-import org.jboss.jandex.DotName;
-import org.jboss.jandex.MethodInfo;
-import org.jboss.jandex.MethodParameterInfo;
-import org.jboss.jandex.Type;
-import org.jboss.logging.Logger;
-
-import graphql.TypeResolutionEnvironment;
-import graphql.scalar.GraphqlStringCoercing;
-import graphql.schema.Coercing;
-import graphql.schema.CoercingParseLiteralException;
-import graphql.schema.CoercingParseValueException;
-import graphql.schema.CoercingSerializeException;
-import graphql.schema.DataFetchingEnvironment;
-import graphql.schema.FieldCoordinates;
-import graphql.schema.GraphQLCodeRegistry;
-import graphql.schema.GraphQLFieldDefinition;
-import graphql.schema.GraphQLObjectType;
-import graphql.schema.GraphQLScalarType;
-import graphql.schema.GraphQLSchema;
-import graphql.schema.GraphQLSchema.Builder;
-import graphql.schema.GraphQLUnionType;
-import io.smallrye.graphql.execution.SchemaPrinter;
-import io.smallrye.graphql.execution.event.EventEmitter;
-import io.smallrye.graphql.federation.api.External;
-import io.smallrye.graphql.federation.api.FederatedSource;
-import io.smallrye.graphql.federation.api.Key;
-import io.smallrye.graphql.schema.ScanningContext;
-import io.smallrye.graphql.schema.model.Argument;
-import io.smallrye.graphql.schema.model.Operation;
-import io.smallrye.graphql.schema.model.Schema;
-
-/**
- * @see GraphQL Federation Spec
- */
-@GraphQLApi
-@ApplicationScoped
-public class Federation {
- private static final Logger LOG = Logger.getLogger(EventEmitter.class);
- private static final DotName KEY = DotName.createSimple(Key.class.getName());
- private static final DotName FEDERATED_SOURCE = DotName.createSimple(FederatedSource.class.getName());
-
- private static final GraphQLScalarType _FieldSet = GraphQLScalarType.newScalar().name("_FieldSet")
- .coercing(new GraphqlStringCoercing()).build();
-
- private static final GraphQLScalarType _Any = GraphQLScalarType.newScalar().name("_Any")
- .coercing(new AnyCoercing()).build();
-
- public static class AnyCoercing implements Coercing
-
+
com.graphql-java
graphql-java
-
+
+ com.apollographql.federation
+ federation-graphql-java-support
+
io.smallrye.reactive
mutiny
-
+
org.jboss.logging
jboss-logging
provided
-
+
org.jboss.logging
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/SmallRyeGraphQLServerLogging.java b/server/implementation/src/main/java/io/smallrye/graphql/SmallRyeGraphQLServerLogging.java
index 22b0cc677..2315e44db 100644
--- a/server/implementation/src/main/java/io/smallrye/graphql/SmallRyeGraphQLServerLogging.java
+++ b/server/implementation/src/main/java/io/smallrye/graphql/SmallRyeGraphQLServerLogging.java
@@ -94,4 +94,7 @@ public interface SmallRyeGraphQLServerLogging {
@Message(id = 15000, value = "Using %s service for context propagation")
void usingContextPropagationService(String name);
+ @LogMessage(level = Logger.Level.DEBUG)
+ @Message(id = 16000, value = "Enable GraphQL Federation")
+ void enableFederation();
}
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java
index 459ca63b8..912225a0d 100644
--- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java
+++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java
@@ -25,6 +25,10 @@
import jakarta.json.JsonReaderFactory;
import jakarta.json.bind.Jsonb;
+import org.eclipse.microprofile.graphql.Name;
+
+import com.apollographql.federation.graphqljava.Federation;
+
import graphql.introspection.Introspection.DirectiveLocation;
import graphql.schema.DataFetcher;
import graphql.schema.FieldCoordinates;
@@ -45,6 +49,7 @@
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLTypeReference;
import graphql.schema.GraphQLUnionType;
+import graphql.schema.TypeResolver;
import graphql.schema.visibility.BlockedFields;
import graphql.schema.visibility.GraphqlFieldVisibility;
import io.smallrye.graphql.SmallRyeGraphQLServerMessages;
@@ -184,7 +189,32 @@ private void generateGraphQLSchema() {
Map overrides = eventEmitter.fireOverrideJsonbConfig();
JsonInputRegistry.override(overrides);
- this.graphQLSchema = schemaBuilder.build();
+ if (Config.get().isFederationEnabled()) {
+ log.enableFederation();
+ GraphQLSchema rawSchema = schemaBuilder.build();
+ this.graphQLSchema = Federation.transform(rawSchema)
+ .fetchEntities(new FederationDataFetcher(rawSchema.getQueryType(), rawSchema.getCodeRegistry()))
+ .resolveEntityType(fetchEntityType())
+ .build();
+ } else {
+ this.graphQLSchema = schemaBuilder.build();
+ }
+ }
+
+ private TypeResolver fetchEntityType() {
+ return env -> {
+ Object src = env.getObject();
+ if (src == null) {
+ return null;
+ }
+ Name annotation = src.getClass().getAnnotation(Name.class);
+ String typeName = (annotation == null) ? src.getClass().getSimpleName() : annotation.value();
+ GraphQLObjectType result = env.getSchema().getObjectType(typeName);
+ if (result == null) {
+ throw new RuntimeException("can't resolve federated entity type " + src.getClass().getName());
+ }
+ return result;
+ };
}
private void createGraphQLDirectiveTypes() {
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java
new file mode 100644
index 000000000..90d93c02d
--- /dev/null
+++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java
@@ -0,0 +1,93 @@
+package io.smallrye.graphql.bootstrap;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.apollographql.federation.graphqljava._Entity;
+
+import graphql.schema.DataFetcher;
+import graphql.schema.DataFetchingEnvironment;
+import graphql.schema.DelegatingDataFetchingEnvironment;
+import graphql.schema.GraphQLArgument;
+import graphql.schema.GraphQLCodeRegistry;
+import graphql.schema.GraphQLFieldDefinition;
+import graphql.schema.GraphQLNamedSchemaElement;
+import graphql.schema.GraphQLObjectType;
+import graphql.schema.GraphQLOutputType;
+
+class FederationDataFetcher implements DataFetcher> {
+
+ private final GraphQLObjectType queryType;
+ private final GraphQLCodeRegistry codeRegistry;
+
+ public FederationDataFetcher(GraphQLObjectType queryType, GraphQLCodeRegistry codeRegistry) {
+ this.queryType = queryType;
+ this.codeRegistry = codeRegistry;
+ }
+
+ @Override
+ public List get(DataFetchingEnvironment environment) throws Exception {
+ return environment.>> getArgument(_Entity.argumentName).stream()
+ .map(representations -> fetchEntities(environment, representations))
+ .collect(toList());
+ }
+
+ private Object fetchEntities(DataFetchingEnvironment env, Map representations) {
+ Map requestedArgs = new HashMap<>(representations);
+ requestedArgs.remove("__typename");
+ String typename = (String) representations.get("__typename");
+ for (GraphQLFieldDefinition field : queryType.getFields()) {
+ if (matchesReturnType(field, typename) && matchesArguments(requestedArgs, field)) {
+ return execute(field, env, requestedArgs);
+ }
+ }
+ throw new RuntimeException("no query found for " + typename + " by " + requestedArgs.keySet());
+ }
+
+ private boolean matchesReturnType(GraphQLFieldDefinition field, String typename) {
+ GraphQLOutputType returnType = field.getType();
+ return returnType instanceof GraphQLNamedSchemaElement
+ && ((GraphQLNamedSchemaElement) returnType).getName().equals(typename);
+ }
+
+ private boolean matchesArguments(Map requestedArguments, GraphQLFieldDefinition field) {
+ Set argumentNames = field.getArguments().stream().map(GraphQLArgument::getName).collect(toSet());
+ return argumentNames.equals(requestedArguments.keySet());
+ }
+
+ private Object execute(GraphQLFieldDefinition field, DataFetchingEnvironment env, Map requestedArgs) {
+ DataFetcher> dataFetcher = codeRegistry.getDataFetcher(queryType, field);
+ DataFetchingEnvironment argsEnv = new DelegatingDataFetchingEnvironment(env) {
+ @Override
+ public Map getArguments() {
+ return requestedArgs;
+ }
+
+ @Override
+ public boolean containsArgument(String name) {
+ return requestedArgs.containsKey(name);
+ }
+
+ @Override
+ public T getArgument(String name) {
+ //noinspection unchecked
+ return (T) requestedArgs.get(name);
+ }
+
+ @Override
+ public T getArgumentOrDefault(String name, T defaultValue) {
+ return containsArgument(name) ? getArgument(name) : defaultValue;
+ }
+ };
+ try {
+ return dataFetcher.get(argsEnv);
+ } catch (Exception e) {
+ throw new RuntimeException("can't fetch data from " + field, e);
+ }
+ }
+}
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java
index 7e20f1642..65e495aca 100644
--- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java
+++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java
@@ -46,6 +46,10 @@ public ExecutionResponse(ExecutionResult executionResult) {
this.executionResult = executionResult;
}
+ public String toString() {
+ return "ExecutionResponse->" + executionResult;
+ }
+
public ExecutionResult getExecutionResult() {
return this.executionResult;
}
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java
index c52d4e391..dc7d8a5eb 100644
--- a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java
+++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java
@@ -13,7 +13,7 @@
/**
* This will load the config service
* Example, using microprofile config
- *
+ *
* @author Phillip Kruger (phillip.kruger@redhat.com)
*/
public interface Config {
@@ -159,6 +159,10 @@ default boolean shouldEmitEvents() {
return isTracingEnabled() || isMetricsEnabled() || isValidationEnabled() || isEventsEnabled();
}
+ default boolean isFederationEnabled() {
+ return false;
+ }
+
default LogPayloadOption logPayload() {
return LogPayloadOption.off;
}
diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java
index d5ac3d4d4..911d43aca 100644
--- a/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java
+++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/TestConfig.java
@@ -15,6 +15,12 @@
*/
public class TestConfig implements Config {
+ public boolean federationEnabled;
+
+ public TestConfig() {
+ reset();
+ }
+
@Override
public boolean isPrintDataFetcherException() {
return true;
@@ -25,6 +31,11 @@ public boolean isEventsEnabled() {
return true;
}
+ @Override
+ public boolean isFederationEnabled() {
+ return federationEnabled;
+ }
+
@Override
public LogPayloadOption logPayload() {
return LogPayloadOption.queryAndVariables;
@@ -55,4 +66,8 @@ public T getConfigValue(String key, Class type, T defaultValue) {
public String getName() {
return "Test Config Service";
}
+
+ public void reset() {
+ this.federationEnabled = false;
+ }
}
diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java
new file mode 100644
index 000000000..34494cc37
--- /dev/null
+++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java
@@ -0,0 +1,12 @@
+package io.smallrye.graphql.schema;
+
+import org.eclipse.microprofile.graphql.GraphQLApi;
+import org.eclipse.microprofile.graphql.Query;
+
+@GraphQLApi
+public class FederationTestApi {
+ @Query
+ public TestTypeWithFederation testTypeWithFederation(String arg) {
+ return null;
+ }
+}
diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java
index 9346307f2..becceb117 100644
--- a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java
+++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java
@@ -15,6 +15,7 @@
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -26,11 +27,20 @@
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import io.smallrye.graphql.api.Directive;
+import io.smallrye.graphql.api.federation.Key;
import io.smallrye.graphql.bootstrap.Bootstrap;
import io.smallrye.graphql.execution.SchemaPrinter;
+import io.smallrye.graphql.execution.TestConfig;
import io.smallrye.graphql.schema.model.Schema;
+import io.smallrye.graphql.spi.config.Config;
class SchemaTest {
+ private final TestConfig config = (TestConfig) Config.get();
+
+ @AfterEach
+ void tearDown() {
+ config.reset();
+ }
@Test
void testSchemaWithDirectives() throws URISyntaxException, IOException {
@@ -55,7 +65,7 @@ void testSchemaWithDirectives() throws URISyntaxException, IOException {
assertEquals("intArrayTestDirective", intArrayTestDirective.getName());
GraphQLArgument argument = intArrayTestDirective.getArgument("value");
assertEquals("value", argument.getName());
- assertArrayEquals(new Object[] { 1, 2, 3 }, (Object[]) argument.getArgumentValue().getValue());
+ assertArrayEquals(new Object[] { 1, 2, 3 }, argument.toAppliedArgument().getValue());
GraphQLFieldDefinition valueField = testTypeWithDirectives.getFieldDefinition("value");
GraphQLDirective fieldDirectiveInstance = valueField.getDirective("fieldDirective");
@@ -82,8 +92,7 @@ void schemaWithEnumDirectives() {
assertNotNull(enumWithDirectives.getValue("A").getDirective("enumDirective"),
"Enum value EnumWithDirectives.A should have directive @enumDirective");
- String schemaString = new SchemaPrinter().print(graphQLSchema);
- assertSchemaEndsWith(schemaString, "" +
+ assertSchemaEndsWith(graphQLSchema, "" +
"enum EnumWithDirectives @enumDirective {\n" +
" A @enumDirective\n" +
" B\n" +
@@ -103,14 +112,54 @@ void schemaWithInputDirectives() {
assertNotNull(inputWithDirectives.getField("bar").getDirective("inputDirective"),
"Input type field InputWithDirectivesInput.bar should have directive @inputDirective");
- String schemaString = new SchemaPrinter().print(graphQLSchema);
- assertSchemaEndsWith(schemaString, "" +
+ assertSchemaEndsWith(graphQLSchema, "" +
"input InputWithDirectivesInput @inputDirective {\n" +
" bar: Int! @inputDirective\n" +
" foo: Int! @inputDirective\n" +
"}\n");
}
+ @Test
+ void testSchemaWithFederationDisabled() {
+ GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class,
+ FederationTestApi.class);
+
+ assertSchemaEndsWith(graphQLSchema, "\n" +
+ "\"Query root\"\n" +
+ "type Query {\n" +
+ " testTypeWithFederation(arg: String): TestTypeWithFederation\n" +
+ "}\n" +
+ "\n" +
+ "type TestTypeWithFederation @key(fields : [\"id\"]) {\n" +
+ " id: String\n" +
+ "}\n");
+ }
+
+ @Test
+ void testSchemaWithFederation() {
+ config.federationEnabled = true;
+ GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class,
+ FederationTestApi.class);
+
+ assertSchemaEndsWith(graphQLSchema, "\n" +
+ "union _Entity = TestTypeWithFederation\n" +
+ "\n" +
+ "\"Query root\"\n" +
+ "type Query {\n" +
+ " _entities(representations: [_Any!]!): [_Entity]!\n" +
+ " _service: _Service!\n" +
+ " testTypeWithFederation(arg: String): TestTypeWithFederation\n" +
+ "}\n" +
+ "\n" +
+ "type TestTypeWithFederation @key(fields : [\"id\"]) {\n" +
+ " id: String\n" +
+ "}\n" +
+ "\n" +
+ "type _Service {\n" +
+ " sdl: String!\n" +
+ "}\n");
+ }
+
private GraphQLSchema createGraphQLSchema(Class>... api) {
Schema schema = SchemaBuilder.build(scan(api));
assertNotNull(schema, "Schema should not be null");
@@ -119,11 +168,13 @@ private GraphQLSchema createGraphQLSchema(Class>... api) {
return graphQLSchema;
}
- private static void assertSchemaContains(String schema, String snippet) {
- assertTrue(schema.contains(snippet), () -> "<<<\n" + schema + "\n>>> does not contain <<<\n" + snippet + "\n>>>");
+ private static void assertSchemaEndsWith(GraphQLSchema schema, String end) {
+ String schemaString = new SchemaPrinter().print(schema);
+ assertSchemaEndsWith(schemaString, end);
}
private static void assertSchemaEndsWith(String schema, String end) {
+ // assertEquals(schema, end); // this is convenient for debugging, as the IDE can show the diff
assertTrue(schema.endsWith(end), () -> "<<<\n" + schema + "\n>>> does not end with <<<\n" + end + "\n>>>");
}
diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java
new file mode 100644
index 000000000..a9d85d831
--- /dev/null
+++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java
@@ -0,0 +1,16 @@
+package io.smallrye.graphql.schema;
+
+import io.smallrye.graphql.api.federation.Key;
+
+@Key(fields = "id")
+public class TestTypeWithFederation {
+ private String id;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git a/server/pom.xml b/server/pom.xml
index 089d585fb..861a9dbe2 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -1,18 +1,22 @@
-
+
4.0.0
-
+
io.smallrye
smallrye-graphql-parent
2.0.0-SNAPSHOT
-
+
smallrye-graphql-server-parent
pom
-
+
SmallRye: GraphQL Server
Server side of the GraphQL Implementation
+
api
@@ -22,7 +26,6 @@
tck
runner
integration-tests
- federation
diff --git a/server/runner/src/main/resources/META-INF/microprofile-config.properties b/server/runner/src/main/resources/META-INF/microprofile-config.properties
index e8ffdb645..40c88b672 100644
--- a/server/runner/src/main/resources/META-INF/microprofile-config.properties
+++ b/server/runner/src/main/resources/META-INF/microprofile-config.properties
@@ -2,7 +2,6 @@ smallrye.graphql.printDataFetcherException=true
smallrye.graphql.tracing.enabled=true
smallrye.graphql.allowGet=true
smallrye.graphql.logPayload=true
-
mp.graphql.showErrorMessage=java.security.AccessControlException,io.smallrye.graphql.test.apps.exceptionlist.*
mp.graphql.hideErrorMessage=java.io.IOException,io.smallrye.graphql.test.apps.exceptionlist.*
-smallrye.graphql.errorExtensionFields=exception,classification,code,description,validationErrorType,queryPath
\ No newline at end of file
+smallrye.graphql.errorExtensionFields=exception,classification,code,description,validationErrorType,queryPath
diff --git a/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java b/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
index 9e814efe6..bc51216e3 100644
--- a/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
+++ b/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
@@ -12,6 +12,7 @@
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import io.smallrye.graphql.api.Entry;
+import io.smallrye.graphql.api.federation.Key;
import io.smallrye.graphql.test.apps.adapt.to.api.AdaptToResource;
import io.smallrye.graphql.test.apps.adapt.with.api.AdapterResource;
import io.smallrye.graphql.test.apps.async.api.AsyncApi;
@@ -23,6 +24,7 @@
import io.smallrye.graphql.test.apps.enumlist.api.EnumListApi;
import io.smallrye.graphql.test.apps.error.api.ErrorApi;
import io.smallrye.graphql.test.apps.exceptionlist.ExceptionListApi;
+import io.smallrye.graphql.test.apps.federation.ProductApi;
import io.smallrye.graphql.test.apps.fieldexistence.api.FieldExistenceApi;
import io.smallrye.graphql.test.apps.generics.api.ControllerWithGenerics;
import io.smallrye.graphql.test.apps.grouping.api.BookGraphQLApi;
@@ -90,6 +92,8 @@ public void process(Archive> applicationArchive, TestClass testClass) {
// For our auto Map adaption
war.addPackage(Entry.class.getPackage());
+ // For the federation directives
+ war.addPackage(Key.class.getPackage());
// Add our own test app
war.addPackage(ProfileGraphQLApi.class.getPackage());
war.addPackage(AdditionalScalarsApi.class.getPackage());
@@ -116,6 +120,7 @@ public void process(Archive> applicationArchive, TestClass testClass) {
war.addPackage(NonNullClass.class.getPackage());
war.addPackage(NonNullPackageClass.class.getPackage());
war.addPackage(StocksApi.class.getPackage());
+ war.addPackage(ProductApi.class.getPackage());
}
}
}
diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductApi.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductApi.java
new file mode 100644
index 000000000..c32167110
--- /dev/null
+++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductApi.java
@@ -0,0 +1,23 @@
+package io.smallrye.graphql.test.apps.federation;
+
+import static java.util.Arrays.asList;
+
+import java.util.List;
+
+import org.eclipse.microprofile.graphql.GraphQLApi;
+import org.eclipse.microprofile.graphql.Id;
+import org.eclipse.microprofile.graphql.Query;
+
+@GraphQLApi
+public class ProductApi {
+ private static final List PRODUCTS = asList(
+ ProductEntity.product("1", "Armchair"),
+ ProductEntity.product("2", "Table"));
+
+ @Query
+ public ProductEntity product(@Id String id) {
+ return PRODUCTS.stream()
+ .filter(product -> product.getId().equals(id))
+ .findFirst().orElseThrow(() -> new RuntimeException("product not find"));
+ }
+}
diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductEntity.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductEntity.java
new file mode 100644
index 000000000..f29c0749a
--- /dev/null
+++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/federation/ProductEntity.java
@@ -0,0 +1,37 @@
+package io.smallrye.graphql.test.apps.federation;
+
+import org.eclipse.microprofile.graphql.Id;
+import org.eclipse.microprofile.graphql.Name;
+
+import io.smallrye.graphql.api.federation.Key;
+
+@Key(fields = "id")
+@Name("Product")
+public class ProductEntity {
+ static ProductEntity product(String id, String name) {
+ ProductEntity product = new ProductEntity();
+ product.setId(id);
+ product.setName(name);
+ return product;
+ }
+
+ @Id
+ private String id;
+ private String name;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/server/tck/src/test/resources/META-INF/microprofile-config.properties b/server/tck/src/test/resources/META-INF/microprofile-config.properties
index ad7b91b3f..cc61e8c73 100644
--- a/server/tck/src/test/resources/META-INF/microprofile-config.properties
+++ b/server/tck/src/test/resources/META-INF/microprofile-config.properties
@@ -6,4 +6,4 @@ smallrye.graphql.logPayload=true
mp.graphql.showErrorMessage=java.security.AccessControlException,io.smallrye.graphql.test.apps.exceptionlist.*
mp.graphql.hideErrorMessage=java.io.IOException,io.smallrye.graphql.test.apps.exceptionlist.*
smallrye.graphql.errorExtensionFields=exception,classification,code,description,validationErrorType,queryPath
-smallrye.graphql.parser.capture.sourceLocation=false
\ No newline at end of file
+smallrye.graphql.parser.capture.sourceLocation=false
diff --git a/server/tck/src/test/resources/tests/federation/input.graphql b/server/tck/src/test/resources/tests/federation/input.graphql
new file mode 100644
index 000000000..d2b130d63
--- /dev/null
+++ b/server/tck/src/test/resources/tests/federation/input.graphql
@@ -0,0 +1,12 @@
+query product {
+ _entities(representations: [{
+ __typename: "Product",
+ id: "1"
+ }]) {
+ ... on Product {
+ __typename
+ name
+ id
+ }
+ }
+}
diff --git a/server/tck/src/test/resources/tests/federation/output.json b/server/tck/src/test/resources/tests/federation/output.json
new file mode 100644
index 000000000..57764221a
--- /dev/null
+++ b/server/tck/src/test/resources/tests/federation/output.json
@@ -0,0 +1,11 @@
+{
+ "data": {
+ "_entities": [
+ {
+ "__typename": "Product",
+ "name": "Armchair",
+ "id": "1"
+ }
+ ]
+ }
+}
diff --git a/server/tck/src/test/resources/tests/federation/test.properties b/server/tck/src/test/resources/tests/federation/test.properties
new file mode 100644
index 000000000..db74c7f9b
--- /dev/null
+++ b/server/tck/src/test/resources/tests/federation/test.properties
@@ -0,0 +1,2 @@
+ignore=false
+priority=100