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 { - @Override - public Object serialize(Object dataFetcherResult) throws CoercingSerializeException { - return dataFetcherResult; - } - - @Override - public Object parseValue(Object input) throws CoercingParseValueException { - return input; - } - - @Override - public Object parseLiteral(Object input) throws CoercingParseLiteralException { - return input; - } - } - - @Inject - Schema schema; - - private GraphQLUnionType _Entity; - private GraphQLFieldDefinition _entities; - - private GraphQLFieldDefinition _service; - private GraphQLObjectType _Service; - private GraphQLFieldDefinition _Service_sdl; - private String rawServiceSchema; - - private GraphQLObjectType.Builder query; - private GraphQLCodeRegistry.Builder codeRegistry; - - private final Map, FederatedEntityResolver> federatedEntityResolvers = new LinkedHashMap<>(); - private final Map, MainEntityResolver> mainEntityResolvers = new LinkedHashMap<>(); - - public GraphQLSchema.Builder beforeSchemaBuild(@Observes GraphQLSchema.Builder builder) { - GraphQLSchema original = builder.build(); - this.rawServiceSchema = rawServiceSchema(original); - - // TODO C: make the query builder available from SmallRye - this.query = GraphQLObjectType.newObject(original.getQueryType()); - // TODO C: make the GraphQLCodeRegistry available from SmallRye - this.codeRegistry = GraphQLCodeRegistry.newCodeRegistry(original.getCodeRegistry()); - - addScalars(builder); - addEntityResolvers(); - addUnions(builder); - addQueries(); - addCode(); - - builder.query(query); - builder.codeRegistry(codeRegistry.build()); - return builder; - } - - /** The SDL without the federation extras (but with the directives) */ - private String rawServiceSchema(GraphQLSchema original) { - try (InputStream stream = getClass().getResourceAsStream("/_service.graphql")) { - if (stream != null) - return new Scanner(stream).useDelimiter("\\Z").next(); - } catch (IOException e) { - throw new RuntimeException("could not load _service.graphql", e); - } - - Builder builder = GraphQLSchema.newSchema(original); - builder.clearDirectives(); - builder.clearSchemaDirectives(); - builder.clearAdditionalTypes(); - return new SchemaPrinter().print(builder.build()) - // TODO C: remove standard directive declarations - .replace("\"Marks the field or enum value as deprecated\"\n" + - "directive @deprecated(\n" + - " \"The reason for the deprecation\"\n" + - " reason: String = \"No longer supported\"\n" + - " ) on FIELD_DEFINITION | ENUM_VALUE\n" + - "\n" + - "\"Exposes a URL that specifies the behaviour of this scalar.\"\n" + - "directive @specifiedBy(\n" + - " \"The URL that specifies the behaviour of this scalar.\"\n" + - " url: String!\n" + - " ) on SCALAR\n", "") - // Apollo doesn't like a single `key` field to be wrapped in square brackets - .replaceAll("@key\\(fields ?: ?\\[([^,\\]]*)]\\)", "@key(fields: $1)"); - } - - private void addScalars(GraphQLSchema.Builder builder) { - builder.additionalType(_Any); - builder.additionalType(_FieldSet); - } - - private void addUnions(GraphQLSchema.Builder builder) { - GraphQLSchema graphQLSchema = builder.build(); - GraphQLObjectType[] entityUnionTypes = ScanningContext.getIndex() - .getAnnotations(KEY).stream() - .map(AnnotationInstance::target) - .map(AnnotationTarget::asClass) - .map(typeInfo -> toObjectType(typeInfo, graphQLSchema)) - .toArray(GraphQLObjectType[]::new); - if (entityUnionTypes.length == 0) - return; - // union _Entity = ... - _Entity = GraphQLUnionType.newUnionType().name("_Entity") - .possibleTypes(entityUnionTypes) - .description("This is a union of all types that use the @key directive, " + - "including both types native to the schema and extended types.") - .build(); - builder.additionalType(_Entity); - } - - private GraphQLObjectType toObjectType(ClassInfo typeInfo, GraphQLSchema graphQLSchema) { - String typeName = typeInfo.name().local(); - GraphQLObjectType objectType = graphQLSchema.getObjectType(typeName); - if (objectType == null) - throw new IllegalStateException("no class registered in schema for " + typeName); - return objectType; - } - - private void addQueries() { - if (_Entity != null) { - // _entities(representations: [_Any!]!): [_Entity]! - this._entities = newFieldDefinition().name("_entities") - .argument(newArgument().name("representations").type(nonNull(list(nonNull(_Any))))) - .type(nonNull(list(_Entity))).build(); - query.field(_entities); - } - - // _service: _Service! - this._Service_sdl = newFieldDefinition().name("sdl").type(nonNull(GraphQLString)) - .description("The sdl representing the federated service capabilities. Includes federation directives, " + - "removes federation types, and includes rest of full schema after schema directives have been applied") - .build(); - this._Service = GraphQLObjectType.newObject() - .name("_Service") - .field(_Service_sdl) - .build(); - this._service = newFieldDefinition().name("_service").type(nonNull(_Service)).build(); - query.field(_service); - } - - private void addCode() { - codeRegistry.typeResolver(_Entity, this::resolveEntity); - codeRegistry.dataFetcher(FieldCoordinates.coordinates(query.build(), _entities), this::fetchEntities); - codeRegistry.dataFetcher(FieldCoordinates.coordinates(query.build(), _service), this::fetchService); - codeRegistry.dataFetcher(FieldCoordinates.coordinates(_Service, _Service_sdl), this::fetchServiceSDL); - } - - public GraphQLObjectType resolveEntity(TypeResolutionEnvironment environment) { - String typeName = environment.getObject().getClass().getSimpleName(); // TODO B: type renames - return environment.getSchema().getObjectType(typeName); - } - - public List fetchEntities(DataFetchingEnvironment environment) { - List> representations = environment.getArgument("representations"); - return representations.stream() - .map(this::resolve) - .collect(toList()); - } - - private Object resolve(Map representation) { - Class type = type(representation); - FederatedEntityResolver federatedEntityResolver = federatedEntityResolvers.get(type); - if (federatedEntityResolver != null) - return federatedEntityResolver.apply(representation); - MainEntityResolver mainEntityResolver = mainEntityResolvers.get(type); - if (mainEntityResolver != null) - return mainEntityResolver.apply(representation); - throw new IllegalStateException("No federated or main entity resolver found for " + type + ". " + - "Add a @Query with the @Key fields as parameters."); - } - - private Class type(Map representation) { - String typeName = (String) representation.get("__typename"); - try { - io.smallrye.graphql.schema.model.Type type = schema.getTypes().get(typeName); - if (type == null) - throw new IllegalStateException("no class registered in schema for " + typeName); - return Class.forName(type.getClassName()); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("can't create extended type instance " + typeName, e); - } - } - - public Object fetchService(DataFetchingEnvironment env) { - return _service; - } - - public String fetchServiceSDL(DataFetchingEnvironment env) { - return rawServiceSchema; - } - - private void addEntityResolvers() { - addMainEntityResolvers(); - addFederatedEntityResolvers(); - } - - private void addMainEntityResolvers() { - this.schema.getQueries().stream() - .filter(this::isMainEntityResolver) - .map(MainEntityResolver::new) - .distinct() - .forEach(mainEntityResolver -> { - LOG.debug("add main entity resolver method " + mainEntityResolver.method); - mainEntityResolvers.put(mainEntityResolver.getType(), mainEntityResolver); - }); - } - - private boolean isMainEntityResolver(Operation operation) { - // TODO C: check type NOT @extends - // TODO B: check (non-optional) method params match @id - return operation.hasArguments(); - } - - private void addFederatedEntityResolvers() { - ScanningContext.getIndex() - .getAnnotations(FEDERATED_SOURCE).stream() - .map(AnnotationInstance::target) - .map(AnnotationTarget::asMethodParameter) - .map(MethodParameterInfo::method) - .map(Federation::toReflectionMethod) - .distinct() - .forEach(method -> { - LOG.debug("add federated entity resolver method " + method); - // TODO C: check that the target type IS @extends - federatedEntityResolvers.put(method.getParameterTypes()[0], new FederatedEntityResolver(schema, method)); - }); - } - - private static Method toReflectionMethod(MethodInfo methodInfo) { - try { - Class declaringClass = Class.forName(methodInfo.declaringClass().name().toString()); - Class[] parameterTypes = methodInfo.parameterTypes().stream() - .map(Type::asClassType) - .map(Type::name) - .map(DotName::toString) - .map(Federation::toClass) - .toArray(Class[]::new); - return declaringClass.getDeclaredMethod(methodInfo.name(), parameterTypes); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("can't find reflection method for " + methodInfo, e); - } - } - - private static Class toClass(String className) { - try { - return Class.forName(className); - } catch (ClassNotFoundException e) { - throw new RuntimeException("class not found: " + className, e); - } - } - - /** A federated query for a type that non-extends type by using its key fields */ - public static class MainEntityResolver implements Function, Object> { - private final Operation operation; - private final Method method; - - private MainEntityResolver(Operation operation) { - this.operation = operation; - this.method = toMethod(operation); - } - - private static Method toMethod(Operation operation) { - return toReflectionMethod(ScanningContext.getIndex() - .getClassByName(DotName.createSimple(operation.getClassName())) - .firstMethod(operation.getMethodName())); - } - - @Override - public Object apply(Map representation) { - Object declaringBean = CDI.current().select(method.getDeclaringClass()).get(); - Object[] args = new Object[method.getParameterCount()]; - for (int i = 0; i < method.getParameterCount(); i++) { - Argument argument = operation.getArguments().get(i); - args[i] = representation.get(argument.getName()); - } - try { - return method.invoke(declaringBean, args); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("invocation of federated entity resolver method failed: " + method, e); - } - } - - public Class getType() { - return method.getReturnType(); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - MainEntityResolver that = (MainEntityResolver) o; - return method.equals(that.method); - } - - @Override - public int hashCode() { - return Objects.hash(method); - } - } - - /** A method annotated as {@link FederatedSource} */ - public static class FederatedEntityResolver implements Function, Object> { - private final Schema schema; - private final Method method; - - private FederatedEntityResolver(Schema schema, Method method) { - this.schema = schema; - this.method = method; - } - - @Override - public Object apply(Map representation) { - // TODO C: batch entity resolvers - Object source = instantiate(representation); - Object value = invoke(source); - set(source, value); - return source; - } - - /** Create a prefilled instance of the type going into the federated entity resolver */ - private Object instantiate(Map representation) { - String typeName = (String) representation.get("__typename"); - try { - io.smallrye.graphql.schema.model.Type type = schema.getTypes().get(typeName); - if (type == null) - throw new IllegalStateException("no class registered in schema for " + typeName); - Class cls = Class.forName(type.getClassName()); - Object instance = cls.getConstructor().newInstance(); - // TODO B: field renames - for (String fieldName : type.getFields().keySet()) { - if ("__typename".equals(fieldName)) - continue; - String value = (String) representation.get(fieldName); - Field field = cls.getDeclaredField(fieldName); - if (field.isAnnotationPresent(External.class)) - LOG.debug("non-external field " + fieldName + " on " + typeName); - - field.setAccessible(true); - field.set(instance, value); - } - return instance; - } catch (ReflectiveOperationException e) { - throw new RuntimeException("can't create extended type instance " + typeName, e); - } - } - - private Object invoke(Object source) { - Object declaringBean = CDI.current().select(method.getDeclaringClass()).get(); - try { - return method.invoke(declaringBean, source); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("invocation of federated entity resolver method failed: " + method, e); - } - } - - private void set(Object source, Object value) { - String fieldName = method.getName(); // TODO B: method renames - try { - Field field = source.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(source, value); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("setting of federated entity resolver field failed: " - + source.getClass().getName() + "#" + fieldName, e); - } - } - } -} diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java index b97c93d13..c6abb642a 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java @@ -2,7 +2,7 @@ /** * All the config options available - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public interface ConfigKey extends org.eclipse.microprofile.graphql.ConfigKey { @@ -14,6 +14,7 @@ public interface ConfigKey extends org.eclipse.microprofile.graphql.ConfigKey { public static final String ENABLE_TRACING = "smallrye.graphql.tracing.enabled"; public static final String ENABLE_VALIDATION = "smallrye.graphql.validation.enabled"; public static final String ENABLE_EVENTS = "smallrye.graphql.events.enabled"; + public static final String ENABLE_FEDERATION = "smallrye.graphql.federation.enabled"; public static final String SCHEMA_INCLUDE_SCALARS = "smallrye.graphql.schema.includeScalars"; public static final String SCHEMA_INCLUDE_DEFINITION = "smallrye.graphql.schema.includeSchemaDefinition"; public static final String SCHEMA_INCLUDE_DIRECTIVES = "smallrye.graphql.schema.includeDirectives"; diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java index ea1623d25..4872d261a 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java @@ -11,7 +11,7 @@ /** * Configuration for GraphQL - * + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class MicroProfileConfig implements Config { @@ -26,6 +26,7 @@ public class MicroProfileConfig implements Config { private Boolean tracingEnabled; private Boolean validationEnabled; private Boolean eventsEnabled; + private Boolean federationEnabled; private Boolean includeScalarsInSchema; private Boolean includeDirectivesInSchema; private Boolean includeSchemaDefinitionInSchema; @@ -133,6 +134,14 @@ public boolean isEventsEnabled() { return eventsEnabled; } + @Override + public boolean isFederationEnabled() { + if (federationEnabled == null) { + federationEnabled = getBooleanConfigValue(ConfigKey.ENABLE_FEDERATION, true); + } + return federationEnabled; + } + @Override public boolean isIncludeScalarsInSchema() { if (includeScalarsInSchema == null) { diff --git a/server/implementation/pom.xml b/server/implementation/pom.xml index e790627e1..2055857a8 100644 --- a/server/implementation/pom.xml +++ b/server/implementation/pom.xml @@ -1,13 +1,16 @@ - + 4.0.0 - + io.smallrye smallrye-graphql-server-parent 2.0.0-SNAPSHOT - + smallrye-graphql SmallRye: GraphQL Server :: Implementation Implementation of the server side of the spec @@ -43,26 +46,29 @@ jakarta.annotation-api provided - + 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