Skip to content

Commit

Permalink
Added resolver for federation queries
Browse files Browse the repository at this point in the history
  • Loading branch information
Roman Lovakov authored and jmartisk committed Oct 1, 2024
1 parent 5ff2b59 commit a076d7d
Show file tree
Hide file tree
Showing 14 changed files with 469 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ private static Map<DotName, AnnotationInstance> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ private Schema generateSchema() {
for (AnnotationInstance graphQLApiAnnotation : graphQLApiAnnotations) {
ClassInfo apiClass = graphQLApiAnnotation.target().asClass();
List<MethodInfo> methods = getAllMethodsIncludingFromSuperClasses(apiClass);
addResolvers(schema, methods);
NamespaceHelper.getNamespace(graphQLApiAnnotation).ifPresentOrElse(
namespace -> addNamespacedOperations(namespace, schema, methods),
() -> addOperations(schema, methods));
Expand Down Expand Up @@ -456,6 +457,19 @@ private void addOperations(Schema schema, List<MethodInfo> methodInfoList) {
}
}

private void addResolvers(Schema schema, List<MethodInfo> methodInfoList) {
for (MethodInfo methodInfo : methodInfoList) {
Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo);
if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.RESOLVER)) {
Operation resolver = operationCreator.createOperation(methodInfo, OperationType.RESOLVER, null);
String className = resolver.getClassName();
String resolverClassName = className.substring(className.lastIndexOf(".") + 1);
resolver.setName(resolverClassName + resolver.getName());
schema.addResolver(resolver);
}
}
}

private void setUpSchemaDirectivesAndDescription(Schema schema,
Collection<AnnotationInstance> graphQLApiAnnotations,
Directives directivesHelper) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ private static DotName getOperationAnnotation(OperationType operationType) {
return Annotations.MUTATION;
case SUBSCRIPTION:
return Annotations.SUBCRIPTION;
case RESOLVER:
return Annotations.RESOLVER;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
public enum OperationType {
QUERY,
MUTATION,
SUBSCRIPTION
SUBSCRIPTION,
RESOLVER,
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public final class Schema implements Serializable {
private Set<Operation> queries = new HashSet<>();
private Set<Operation> mutations = new HashSet<>();
private Set<Operation> subscriptions = new HashSet<>();
private Set<Operation> resolvers = new HashSet<>();

private Map<String, NamespaceContainer> namespacedQueries = new HashMap<>();
private Map<String, NamespaceContainer> namespacedMutations = new HashMap<>();
Expand Down Expand Up @@ -99,7 +100,7 @@ public void addQuery(Operation query) {
public boolean hasOperations() {
return hasQueries() || hasNamespaceQueries()
|| hasMutations() || hasNamespaceMutations()
|| hasSubscriptions();
|| hasSubscriptions() || hasResolvers();
}

public boolean hasQueries() {
Expand Down Expand Up @@ -138,6 +139,22 @@ public boolean hasSubscriptions() {
return !this.subscriptions.isEmpty();
}

public Set<Operation> getResolvers() {
return resolvers;
}

public void setResolvers(Set<Operation> resolvers) {
this.resolvers = resolvers;
}

public void addResolver(Operation resolver) {
this.resolvers.add(resolver);
}

public boolean hasResolvers() {
return !this.resolvers.isEmpty();
}

public Map<String, InputType> getInputs() {
return inputs;
}
Expand Down Expand Up @@ -342,6 +359,7 @@ public String toString() {
", queries=" + queries +
", mutations=" + mutations +
", subscriptions=" + subscriptions +
", resolvers=" + resolvers +
", namespacedQueries=" + namespacedQueries +
", namespacedMutations=" + namespacedMutations +
", directiveTypes=" + directiveTypes +
Expand Down
119 changes: 119 additions & 0 deletions docs/federation.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,122 @@ public class Prices {

It is crucial that the sequence of argument list matches with the order of result list. Currently, the name of the Argument `id` must match with the property name in the type.

## Federation Reference Resolver

In federation you also may want extend external type by some fields, without publishing queries into schema. You can do it using @Resolver

```java
@Extends
@Key(fields = @FieldSet("upc"))
public final class Product {
@External
@NonNull
private String upc;
@External
private Integer weight;
@External
private Integer price;
private Boolean inStock;
@Requires(fields = @FieldSet("price weight"))
private Integer shippingPrice;
}

@GraphQLApi
public class Api {
@Query // 0 query, that will be added into schema
public Product findByUPC(String upc) {
return new Product(upc , ...etc);
}

@Resolver // 1 You dont receive external fields price weight here, just key
public Product resolveByUPC(String upc) {
return new Product(upc , ...etc);
}

@Resolver // 2 The order of variables doesn't matter
public Product resolveByUPCForShipping(int price, String upc, @Name("weight") int someWeight) {
return new Product(upc , someWeight, price, (price * someWeight) /*calculate shippingPrice */, ...etc);
}

@Resolver // 3
public Product resolveByUPCForSource(int price, String upc) {
return new Product(upc, price, ...etc);
}

@Requires(fields = @FieldSet("price"))
public int anotherWeight(@Source Product product) {
return product.price() * 2;
}
}
```

Will be generated next schema
```
type Product @extends @key(fields : "upc") {
anotherWeight: Int! @requires(fields : "price")
inStock: Boolean
price: Int @external
shippingPrice: Int @requires(fields : "price weight")
upc: String! @external
weight: Int @external
}
type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
```

These methods will only be available to the federation router, which send next request
```
// request 1
query {
_entities(representations: [{
"__typename": "Product",
"upc": "1" // just id key
}]) {
__typename
... on Product {
inStock
}
}
}
// request 2
query {
_entities(representations: [{
"__typename": "Product",
"upc": "1", // id key
"price": 100, // shippingPrice requires this field
"weight": 100 // shippingPrice requires this field
}]) {
__typename
... on Product {
inStock
shippingPrice
}
}
}
// request 3
query {
_entities(representations: [{
"__typename": "Product",
"upc": "2",
"price": 1299 // anotherWeight requires this field
}
]) {
__typename
... on Product {
anotherWeight
}
}
}
```

Unfortunately, you will have to make separate methods with different `@External` parameters.

It is not currently possible to combine them into one separate type.

You also can using @Query (if you want add queries into schema) or @Resolver (requests 0 and 1).
And if it was request `_entities` - @Resolvers methods are checked first (they have higher priority).
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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;
Expand Down Expand Up @@ -127,6 +128,7 @@ private IndexView createCustomIndex() {
indexer.index(convertClassToInputStream(Shareable.class));
indexer.index(convertClassToInputStream(Tag.class));
indexer.index(convertClassToInputStream(Namespace.class));
indexer.index(convertClassToInputStream(Resolver.class));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import com.apollographql.federation.graphqljava.Federation;

import graphql.Scalars;
import graphql.introspection.Introspection.DirectiveLocation;
import graphql.schema.Coercing;
import graphql.schema.DataFetcher;
Expand Down Expand Up @@ -180,7 +181,7 @@ private void generateGraphQLSchema() {
createGraphQLObjectTypes();
createGraphQLInputObjectTypes();

addQueries(schemaBuilder);
GraphQLObjectType queryRootType = addQueries(schemaBuilder);
addMutations(schemaBuilder);
addSubscriptions(schemaBuilder);
schemaBuilder.withSchemaAppliedDirectives(Arrays.stream(
Expand Down Expand Up @@ -209,9 +210,18 @@ private void generateGraphQLSchema() {

if (Config.get().isFederationEnabled()) {
log.enableFederation();

// hack! For schema build success if queries are empty.
// It will be overrides in Federation transformation
addDummySdlQuery(schemaBuilder, queryRootType);

// Build reference resolvers type, without adding to schema (just for federation)
GraphQLObjectType resolversType = buildResolvers();

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();
Expand All @@ -220,6 +230,35 @@ private void generateGraphQLSchema() {
}
}

private void addDummySdlQuery(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();
Expand Down Expand Up @@ -321,7 +360,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);
Expand All @@ -335,6 +374,7 @@ private void addQueries(GraphQLSchema.Builder schemaBuilder) {

GraphQLObjectType query = queryBuilder.build();
schemaBuilder.query(query);
return query;
}

private void addMutations(GraphQLSchema.Builder schemaBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ public class FederationDataFetcher implements DataFetcher<CompletableFuture<List

public static final String TYPENAME = "__typename";
private final GraphQLObjectType queryType;
private final GraphQLObjectType resolversType;
private final GraphQLCodeRegistry codeRegistry;
private final ConcurrentHashMap<TypeAndArgumentNames, TypeFieldWrapper> cache = new ConcurrentHashMap<>();

public FederationDataFetcher(GraphQLObjectType queryType, GraphQLCodeRegistry codeRegistry) {
public FederationDataFetcher(GraphQLObjectType resolversType, GraphQLObjectType queryType,
GraphQLCodeRegistry codeRegistry) {
this.resolversType = resolversType;
this.queryType = queryType;
this.codeRegistry = codeRegistry;
}
Expand Down Expand Up @@ -104,6 +107,11 @@ && matchesArguments(typeAndArgumentNames, definition)) {
}

private TypeFieldWrapper findBatchFieldDefinition(TypeAndArgumentNames typeAndArgumentNames) {
for (GraphQLFieldDefinition field : resolversType.getFields()) {
if (matchesReturnTypeList(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) {
return new TypeFieldWrapper(resolversType, field);
}
}
for (GraphQLFieldDefinition field : queryType.getFields()) {
if (matchesReturnTypeList(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) {
return new TypeFieldWrapper(queryType, field);
Expand All @@ -120,6 +128,11 @@ private TypeFieldWrapper findBatchFieldDefinition(TypeAndArgumentNames typeAndAr
}

private TypeFieldWrapper findFieldDefinition(TypeAndArgumentNames typeAndArgumentNames) {
for (GraphQLFieldDefinition field : resolversType.getFields()) {
if (matchesReturnType(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) {
return new TypeFieldWrapper(resolversType, field);
}
}
for (GraphQLFieldDefinition field : queryType.getFields()) {
if (matchesReturnType(field, typeAndArgumentNames.type) && matchesArguments(typeAndArgumentNames, field)) {
return new TypeFieldWrapper(queryType, field);
Expand All @@ -132,7 +145,6 @@ private TypeFieldWrapper findFieldDefinition(TypeAndArgumentNames typeAndArgumen
return typeFieldWrapper;
}
}

throw new RuntimeException(
"no query found for " + typeAndArgumentNames.type + " by " + typeAndArgumentNames.argumentNames);
}
Expand Down
Loading

0 comments on commit a076d7d

Please sign in to comment.