From a87737b9a7369974693b056db56d226e2987ee90 Mon Sep 17 00:00:00 2001 From: Roman Lovakov Date: Wed, 4 Sep 2024 18:57:20 +0300 Subject: [PATCH] Added resolver for federation queries --- .../smallrye/graphql/schema/Annotations.java | 1 + .../graphql/schema/SchemaBuilder.java | 3 ++ .../schema/creator/OperationCreator.java | 2 + .../graphql/schema/model/OperationType.java | 3 +- .../smallrye/graphql/schema/model/Schema.java | 17 ++++++ .../graphql/api/federation/Resolver.java | 15 ++++++ .../graphql/entry/http/IndexInitializer.java | 2 + .../smallrye/graphql/bootstrap/Bootstrap.java | 54 +++++++++++++++++-- .../bootstrap/FederationDataFetcher.java | 16 +++++- 9 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 server/api/src/main/java/io/smallrye/graphql/api/federation/Resolver.java diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java index 3123a0d64..971dd5525 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java @@ -590,6 +590,7 @@ private static Map getAnnotationsWithFilter(org.jbo public static final DotName ERROR_CODE = DotName.createSimple("io.smallrye.graphql.api.ErrorCode"); public static final DotName DATAFETCHER = DotName.createSimple("io.smallrye.graphql.api.DataFetcher"); public static final DotName SUBCRIPTION = DotName.createSimple("io.smallrye.graphql.api.Subscription"); + public static final DotName RESOLVER = DotName.createSimple("io.smallrye.graphql.api.federation.Resolver"); public static final DotName DIRECTIVE = DotName.createSimple("io.smallrye.graphql.api.Directive"); public static final DotName DEFAULT_NON_NULL = DotName.createSimple("io.smallrye.graphql.api.DefaultNonNull"); public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable"); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java index 0d819a3f1..39566d1a4 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java @@ -386,6 +386,9 @@ private void addOperations(Optional group, Schema schema, List queries = new HashSet<>(); private Set mutations = new HashSet<>(); private Set subscriptions = new HashSet<>(); + private Set resolvers = new HashSet<>(); private Map> groupedQueries = new HashMap<>(); private Map> groupedMutations = new HashMap<>(); @@ -95,6 +96,22 @@ public boolean hasSubscriptions() { return !this.subscriptions.isEmpty(); } + public Set getResolvers() { + return resolvers; + } + + public void setResolvers(Set resolvers) { + this.resolvers = resolvers; + } + + public void addResolver(Operation resolver) { + this.resolvers.add(resolver); + } + + public boolean hasResolvers() { + return !this.resolvers.isEmpty(); + } + public Map> getGroupedQueries() { return groupedQueries; } diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Resolver.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Resolver.java new file mode 100644 index 000000000..98e617a14 --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Resolver.java @@ -0,0 +1,15 @@ +package io.smallrye.graphql.api.federation; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.smallrye.common.annotation.Experimental; + +@Target(ElementType.METHOD) +@Retention(RUNTIME) +@Experimental("Resolver method without creating query method") +public @interface Resolver { +} diff --git a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java index 92610c307..6a4a189ce 100644 --- a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java +++ b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java @@ -41,6 +41,7 @@ import io.smallrye.graphql.api.federation.Override; import io.smallrye.graphql.api.federation.Provides; import io.smallrye.graphql.api.federation.Requires; +import io.smallrye.graphql.api.federation.Resolver; import io.smallrye.graphql.api.federation.Shareable; import io.smallrye.graphql.api.federation.Tag; import io.smallrye.graphql.api.federation.link.Import; @@ -126,6 +127,7 @@ private IndexView createCustomIndex() { indexer.index(convertClassToInputStream(Shareable.class)); indexer.index(convertClassToInputStream(Tag.class)); indexer.index(convertClassToInputStream(OneOf.class)); + indexer.index(convertClassToInputStream(Resolver.class)); } catch (IOException ex) { throw new RuntimeException(ex); } 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 1eaf74ff6..45225b7b1 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 @@ -30,6 +30,7 @@ import com.apollographql.federation.graphqljava.Federation; +import graphql.Scalars; import graphql.introspection.Introspection.DirectiveLocation; import graphql.schema.Coercing; import graphql.schema.DataFetcher; @@ -124,7 +125,7 @@ public static GraphQLSchema bootstrap(Schema schema) { } public static GraphQLSchema bootstrap(Schema schema, boolean skipInjectionValidation) { - if (schema != null && (schema.hasOperations())) { + if (schema != null && (schema.hasOperations()) || Config.get().isFederationEnabled()) { Bootstrap bootstrap = new Bootstrap(schema, skipInjectionValidation); bootstrap.generateGraphQLSchema(); return bootstrap.graphQLSchema; @@ -185,7 +186,11 @@ private void generateGraphQLSchema() { createGraphQLObjectTypes(); createGraphQLInputObjectTypes(); - addQueries(schemaBuilder); + GraphQLObjectType resolversType = Config.get().isFederationEnabled() + ? buildResolvers() + : GraphQLObjectType.newObject().name("Resolver").build(); + + GraphQLObjectType queryRootType = addQueries(schemaBuilder); addMutations(schemaBuilder); addSubscriptions(schemaBuilder); schemaBuilder.withSchemaAppliedDirectives(Arrays.stream( @@ -214,9 +219,20 @@ private void generateGraphQLSchema() { if (Config.get().isFederationEnabled()) { log.enableFederation(); + + if (!(schema.hasQueries() || schema.hasResolvers() || schema.hasMutations() || schema.hasSubscriptions())) { + throw new RuntimeException("You must define at least one method marked with one of the annotations - " + + "@Query, @Mutation, @Subscription, @Resolver. An empty diagram has no meaning"); + } + + // hack! For schema build success if queries are empty. + // It will be overrides in Federation transformation + addSdlQueryToSchema(schemaBuilder, queryRootType); + GraphQLSchema rawSchema = schemaBuilder.build(); this.graphQLSchema = Federation.transform(rawSchema) - .fetchEntities(new FederationDataFetcher(rawSchema.getQueryType(), rawSchema.getCodeRegistry())) + .fetchEntities( + new FederationDataFetcher(resolversType, rawSchema.getQueryType(), rawSchema.getCodeRegistry())) .resolveEntityType(fetchEntityType()) .setFederation2(true) .build(); @@ -225,6 +241,35 @@ private void generateGraphQLSchema() { } } + private void addSdlQueryToSchema(GraphQLSchema.Builder schemaBuilder, GraphQLObjectType queryRootType) { + GraphQLObjectType type = GraphQLObjectType.newObject() + .name("_Service") + .field(GraphQLFieldDefinition + .newFieldDefinition().name("sdl") + .type(new GraphQLNonNull(Scalars.GraphQLString)) + .build()) + .build(); + + GraphQLFieldDefinition field = GraphQLFieldDefinition.newFieldDefinition() + .name("_service") + .type(GraphQLNonNull.nonNull(type)) + .build(); + + GraphQLObjectType.Builder newQueryType = GraphQLObjectType.newObject(queryRootType); + + newQueryType.field(field); + schemaBuilder.query(newQueryType.build()); + } + + private GraphQLObjectType buildResolvers() { + GraphQLObjectType.Builder queryBuilder = GraphQLObjectType.newObject() + .name("Resolver"); + if (schema.hasResolvers()) { + addRootObject(queryBuilder, schema.getResolvers(), "Resolver"); + } + return queryBuilder.build(); + } + private TypeResolver fetchEntityType() { return env -> { Object src = env.getObject(); @@ -326,7 +371,7 @@ private void createGraphQLDirectiveType(DirectiveType directiveType) { directiveTypes.add(directiveBuilder.build()); } - private void addQueries(GraphQLSchema.Builder schemaBuilder) { + private GraphQLObjectType addQueries(GraphQLSchema.Builder schemaBuilder) { GraphQLObjectType.Builder queryBuilder = GraphQLObjectType.newObject() .name(QUERY) .description(QUERY_DESCRIPTION); @@ -340,6 +385,7 @@ private void addQueries(GraphQLSchema.Builder schemaBuilder) { GraphQLObjectType query = queryBuilder.build(); schemaBuilder.query(query); + return query; } private void addMutations(GraphQLSchema.Builder schemaBuilder) { 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 index 43d14724a..03db934e5 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java @@ -38,10 +38,13 @@ public class FederationDataFetcher implements DataFetcher cache = new HashMap<>(); - public FederationDataFetcher(GraphQLObjectType queryType, GraphQLCodeRegistry codeRegistry) { + public FederationDataFetcher(GraphQLObjectType resolversType, GraphQLObjectType queryType, + GraphQLCodeRegistry codeRegistry) { + this.resolversType = resolversType; this.queryType = queryType; this.codeRegistry = codeRegistry; } @@ -115,6 +118,11 @@ private TypeFieldWrapper findBatchFieldDefinition(TypeAndArgumentNames typeAndAr return typeFieldWrapper; } } + for (GraphQLFieldDefinition field : resolversType.getFields()) { + if (matchesReturnTypeList(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) { + return new TypeFieldWrapper(resolversType, field); + } + } return null; } @@ -131,7 +139,11 @@ private TypeFieldWrapper findFieldDefinition(TypeAndArgumentNames typeAndArgumen return typeFieldWrapper; } } - + for (GraphQLFieldDefinition field : resolversType.getFields()) { + if (matchesReturnType(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) { + return new TypeFieldWrapper(resolversType, field); + } + } throw new RuntimeException( "no query found for " + typeAndArgumentNames.type + " by " + typeAndArgumentNames.argumentNames); }