Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling federation _entities queries without creating @Query #2180

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading