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

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
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 @@ -386,6 +386,9 @@ private void addOperations(Optional<Group> group, Schema schema, List<MethodInfo
} else {
schema.addSubscription(subscription);
}
} else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.RESOLVER)) {
Operation resolver = operationCreator.createOperation(methodInfo, OperationType.RESOLVER, null);
schema.addResolver(resolver);
}
}
}
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 @@ -21,6 +21,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<Group, Set<Operation>> groupedQueries = new HashMap<>();
private Map<Group, Set<Operation>> groupedMutations = new HashMap<>();
Expand Down Expand Up @@ -95,6 +96,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<Group, Set<Operation>> getGroupedQueries() {
return groupedQueries;
}
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 @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -185,7 +186,7 @@ private void generateGraphQLSchema() {
createGraphQLObjectTypes();
createGraphQLInputObjectTypes();

addQueries(schemaBuilder);
GraphQLObjectType queryRootType = addQueries(schemaBuilder);
addMutations(schemaBuilder);
addSubscriptions(schemaBuilder);
schemaBuilder.withSchemaAppliedDirectives(Arrays.stream(
Expand Down Expand Up @@ -214,9 +215,23 @@ 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
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 @@ -225,6 +240,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 @@ -326,7 +370,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 @@ -340,6 +384,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 @@ -38,10 +38,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 HashMap<TypeAndArgumentNames, TypeFieldWrapper> 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;
}
Expand Down Expand Up @@ -103,6 +106,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 @@ -119,6 +127,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 @@ -131,7 +144,6 @@ private TypeFieldWrapper findFieldDefinition(TypeAndArgumentNames typeAndArgumen
return typeFieldWrapper;
}
}

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