Skip to content

Commit 2e4bfe8

Browse files
Merge pull request #75 from intuit/nested-fieldresolver
Adding support for nested field resolver
2 parents 6bfa0b5 + 6c7646c commit 2e4bfe8

File tree

10 files changed

+372
-65
lines changed

10 files changed

+372
-65
lines changed

src/main/java/com/intuit/graphql/orchestrator/batch/FieldResolverBatchLoader.java

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,42 @@
33
import static com.intuit.graphql.orchestrator.GraphQLOrchestrator.DATA_LOADER_REGISTRY_CONTEXT_KEY;
44
import static com.intuit.graphql.orchestrator.resolverdirective.FieldResolverDirectiveUtil.createFieldResolverOperationName;
55
import static graphql.language.AstPrinter.printAstCompact;
6+
import static graphql.schema.GraphQLTypeUtil.unwrapAll;
67

8+
import com.intuit.graphql.orchestrator.ServiceProvider;
9+
import com.intuit.graphql.orchestrator.datafetcher.ServiceDataFetcher;
710
import com.intuit.graphql.orchestrator.fieldresolver.FieldResolverBatchSelectionSetSupplier;
11+
import com.intuit.graphql.orchestrator.fieldresolver.FieldResolverGraphQLError;
812
import com.intuit.graphql.orchestrator.fieldresolver.QueryOperationFactory;
913
import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveDefinition;
1014
import com.intuit.graphql.orchestrator.schema.GraphQLObjects;
15+
import com.intuit.graphql.orchestrator.schema.ServiceMetadata;
1116
import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext;
17+
import com.intuit.graphql.orchestrator.xtext.FieldContext;
1218
import graphql.ExecutionInput;
13-
import graphql.ExecutionResult;
14-
import graphql.GraphQL;
1519
import graphql.GraphQLContext;
16-
import graphql.execution.AsyncExecutionStrategy;
1720
import graphql.execution.DataFetcherResult;
21+
import graphql.language.AstTransformer;
1822
import graphql.language.Definition;
1923
import graphql.language.Document;
2024
import graphql.language.FragmentDefinition;
2125
import graphql.language.FragmentSpread;
2226
import graphql.language.OperationDefinition;
2327
import graphql.language.SelectionSet;
28+
import graphql.schema.DataFetcher;
2429
import graphql.schema.DataFetchingEnvironment;
30+
import graphql.schema.FieldCoordinates;
31+
import graphql.schema.GraphQLCodeRegistry;
32+
import graphql.schema.GraphQLFieldDefinition;
33+
import graphql.schema.GraphQLFieldsContainer;
34+
import graphql.schema.GraphQLObjectType;
2535
import graphql.schema.GraphQLSchema;
2636
import java.util.Collections;
2737
import java.util.List;
38+
import java.util.Map;
2839
import java.util.Objects;
2940
import java.util.concurrent.CompletableFuture;
3041
import java.util.concurrent.CompletionStage;
31-
import java.util.function.Function;
3242
import java.util.stream.Collectors;
3343
import lombok.Builder;
3444
import org.apache.commons.collections4.CollectionUtils;
@@ -37,11 +47,6 @@
3747

3848
public class FieldResolverBatchLoader implements BatchLoader<DataFetchingEnvironment, DataFetcherResult<Object>> {
3949

40-
private static final Function<GraphQLSchema, GraphQL> DEFAULT_GRAPHQL_BUILDER = schema -> GraphQL
41-
.newGraphQL(schema)
42-
.queryExecutionStrategy(new AsyncExecutionStrategy())
43-
.build();
44-
4550
private final QueryOperationModifier queryOperationModifier = new QueryOperationModifier();
4651

4752
private final QueryResponseModifier queryResponseModifier = new DefaultQueryResponseModifier();
@@ -54,6 +59,8 @@ public class FieldResolverBatchLoader implements BatchLoader<DataFetchingEnviron
5459

5560
private final QueryOperationFactory queryOperationFactory = new QueryOperationFactory();
5661

62+
private static final AstTransformer AST_TRANSFORMER = new AstTransformer();
63+
5764
@Builder
5865
public FieldResolverBatchLoader(FieldResolverContext fieldResolverContext) {
5966
Objects.requireNonNull(fieldResolverContext, "fieldResolverContext is required");
@@ -71,31 +78,67 @@ public FieldResolverBatchLoader(FieldResolverContext fieldResolverContext) {
7178
public CompletionStage<List<DataFetcherResult<Object>>> load(final List<DataFetchingEnvironment> dataFetchingEnvironments) {
7279

7380
String originalOperationName = dataFetchingEnvironments.get(0).getOperationDefinition().getName();
74-
String operationName = createFieldResolverOperationName(originalOperationName);
81+
String downstreamQueryOpName = createFieldResolverOperationName(originalOperationName);
7582

7683
FieldResolverBatchSelectionSetSupplier fieldResolverBatchSelectionSetSupplier =
7784
new FieldResolverBatchSelectionSetSupplier(resolverSelectedFields, dataFetchingEnvironments, fieldResolverContext);
85+
SelectionSet selectionSet = fieldResolverBatchSelectionSetSupplier.get();
86+
OperationDefinition downstreamQueryOpDef = queryOperationFactory.create(downstreamQueryOpName, selectionSet);
7887

79-
OperationDefinition resolverQueryOpDef = queryOperationFactory.create(operationName, fieldResolverBatchSelectionSetSupplier);
88+
ServiceMetadata serviceMetadata = getServiceMetadata(dataFetchingEnvironments.get(0));
89+
if (serviceMetadata.hasFieldResolverDirective()) {
90+
downstreamQueryOpDef = removeExternalFields(downstreamQueryOpDef, dataFetchingEnvironments.get(0), serviceMetadata);
91+
}
8092

8193
if (this.fieldResolverContext.isRequiresTypeNameInjection()) {
82-
resolverQueryOpDef = queryOperationModifier.modifyQuery(
94+
downstreamQueryOpDef = queryOperationModifier.modifyQuery(
8395
dataFetchingEnvironments.get(0).getGraphQLSchema(),
84-
resolverQueryOpDef,
96+
downstreamQueryOpDef,
8597
// each DFE have identical fragmentsByName since this is a batch call for same field
8698
// Arguments on the field with @resolver now allowed, set variables empty
8799
dataFetchingEnvironments.get(0).getFragmentsByName(), Collections.emptyMap());
88100
}
89101

90-
List<Definition<FragmentDefinition>> resolverQueryFragmentDefinitions =
102+
List<Definition<FragmentDefinition>> downstreamQueryFragmentDefinitions =
91103
createResolverQueryFragmentDefinitions(dataFetchingEnvironments.get(0));
92104

93-
return execute(dataFetchingEnvironments.get(0), resolverQueryOpDef, resolverQueryFragmentDefinitions)
94-
.thenApply(ExecutionResult::toSpecification)
105+
ServiceProvider serviceProvider = serviceMetadata.getServiceProvider();
106+
return execute(dataFetchingEnvironments.get(0), downstreamQueryOpDef, downstreamQueryFragmentDefinitions, serviceProvider)
95107
.thenApply(queryResponseModifier::modify)
96108
.thenApply(result -> batchResultTransformer.toBatchResult(result, dataFetchingEnvironments));
97109
}
98110

111+
private OperationDefinition removeExternalFields(OperationDefinition operationDefinition,
112+
DataFetchingEnvironment dataFetchingEnvironment, ServiceMetadata serviceMetadata) {
113+
GraphQLSchema graphQLSchema = dataFetchingEnvironment.getGraphQLSchema();
114+
GraphQLObjectType rootType = graphQLSchema.getQueryType();
115+
Map<String, FragmentDefinition> fragmentsByName = dataFetchingEnvironment.getFragmentsByName();
116+
return (OperationDefinition) AST_TRANSFORMER.transform(operationDefinition,
117+
new NoExternalReferenceSelectionSetModifier((GraphQLFieldsContainer) unwrapAll(rootType), serviceMetadata, fragmentsByName));
118+
}
119+
120+
private ServiceMetadata getServiceMetadata(DataFetchingEnvironment dataFetchingEnvironment) {
121+
GraphQLCodeRegistry graphQLCodeRegistry = dataFetchingEnvironment.getGraphQLSchema().getCodeRegistry();
122+
GraphQLFieldDefinition graphQLFieldDefinition = dataFetchingEnvironment.getFieldDefinition();
123+
124+
FieldContext fieldContext = this.fieldResolverContext.getTargetFieldContext();
125+
FieldCoordinates fieldCoordinates = fieldContext.getFieldCoordinates();
126+
127+
DataFetcher<?> dataFetcher = graphQLCodeRegistry.getDataFetcher(fieldCoordinates, graphQLFieldDefinition);
128+
if (Objects.nonNull(dataFetcher) && dataFetcher instanceof ServiceDataFetcher) {
129+
ServiceDataFetcher serviceDataFetcher = (ServiceDataFetcher) dataFetcher;
130+
return serviceDataFetcher.getServiceMetadata();
131+
} else {
132+
throw FieldResolverGraphQLError.builder()
133+
.errorMessage("ServiceDataFetcher not found.")
134+
.fieldName(fieldCoordinates.getFieldName())
135+
.parentTypeName(fieldCoordinates.getTypeName())
136+
.resolverDirectiveDefinition(this.fieldResolverContext.getResolverDirectiveDefinition())
137+
.serviceNameSpace(fieldResolverContext.getServiceNamespace())
138+
.build();
139+
}
140+
}
141+
99142
private List<Definition<FragmentDefinition>> createResolverQueryFragmentDefinitions(DataFetchingEnvironment dataFetchingEnvironment) {
100143
SelectionSet selectionSet = dataFetchingEnvironment.getField().getSelectionSet();
101144
if (selectionSet == null || CollectionUtils.isEmpty(selectionSet.getSelections())) {
@@ -109,9 +152,10 @@ private List<Definition<FragmentDefinition>> createResolverQueryFragmentDefiniti
109152
.collect(Collectors.toList());
110153
}
111154

112-
private CompletableFuture<ExecutionResult> execute(DataFetchingEnvironment dataFetchingEnvironment,
155+
private CompletableFuture<Map<String, Object>> execute(DataFetchingEnvironment dataFetchingEnvironment,
113156
OperationDefinition resolverQueryOpDef,
114-
List<Definition<FragmentDefinition>> resolverQueryFragmentDefs
157+
List<Definition<FragmentDefinition>> resolverQueryFragmentDefs,
158+
ServiceProvider serviceProvider
115159
) {
116160
GraphQLContext context = dataFetchingEnvironment.getContext();
117161

@@ -129,9 +173,7 @@ private CompletableFuture<ExecutionResult> execute(DataFetchingEnvironment dataF
129173
.operationName(resolverQueryOpDef.getName())
130174
.build();
131175

132-
GraphQL graphQL = DEFAULT_GRAPHQL_BUILDER.apply(dataFetchingEnvironment.getGraphQLSchema());
133-
134-
return graphQL.executeAsync(resolverQueryExecutionInput);
176+
return serviceProvider.query(resolverQueryExecutionInput, dataFetchingEnvironment.getLocalContext());
135177
}
136178

137179
}

src/main/java/com/intuit/graphql/orchestrator/batch/NoExternalReferenceSelectionSetModifier.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import graphql.language.InlineFragment;
1616
import graphql.language.Node;
1717
import graphql.language.NodeVisitorStub;
18+
import graphql.language.OperationDefinition;
1819
import graphql.language.SelectionSet;
1920
import graphql.schema.FieldCoordinates;
2021
import graphql.schema.GraphQLFieldDefinition;
@@ -89,6 +90,12 @@ public TraversalControl visitInlineFragment(InlineFragment node, TraverserContex
8990
return TraversalControl.CONTINUE;
9091
}
9192

93+
@Override
94+
public TraversalControl visitOperationDefinition(OperationDefinition node, TraverserContext<Node> context) {
95+
context.setVar(GraphQLType.class, rootType);
96+
return TraversalControl.CONTINUE;
97+
}
98+
9299
@Override
93100
public TraversalControl visitSelectionSet(SelectionSet node, TraverserContext<Node> context) {
94101
GraphQLFieldsContainer parentType = (GraphQLFieldsContainer) getParentType(context);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.intuit.graphql.orchestrator.fieldresolver;
2+
3+
import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveDefinition;
4+
import graphql.ErrorType;
5+
import graphql.GraphqlErrorException;
6+
7+
/**
8+
* A class thrown when an error occured during field resolver graphql execution
9+
*/
10+
public class FieldResolverGraphQLError extends GraphqlErrorException {
11+
12+
private FieldResolverGraphQLError(FieldResolverGraphQLError.Builder builder) {
13+
super(builder);
14+
}
15+
16+
@Override
17+
public ErrorType getErrorType() {
18+
return ErrorType.ExecutionAborted;
19+
}
20+
21+
public static FieldResolverGraphQLError.Builder builder() {
22+
return new FieldResolverGraphQLError.Builder();
23+
}
24+
25+
public static class Builder extends BuilderBase<FieldResolverGraphQLError.Builder, FieldResolverGraphQLError> {
26+
27+
private String errorMessage;
28+
29+
private String fieldName;
30+
private String parentTypeName;
31+
private ResolverDirectiveDefinition resolverDirectiveDefinition;
32+
private String serviceNameSpace;
33+
34+
public Builder fieldName(String fieldName) {
35+
this.fieldName = fieldName;
36+
return this;
37+
}
38+
39+
public Builder parentTypeName(String parentTypeName) {
40+
this.parentTypeName = parentTypeName;
41+
return this;
42+
}
43+
44+
public Builder resolverDirectiveDefinition(ResolverDirectiveDefinition resolverDirectiveDefinition) {
45+
this.resolverDirectiveDefinition = resolverDirectiveDefinition;
46+
return this;
47+
}
48+
49+
public Builder serviceNameSpace(String serviceNameSpace) {
50+
this.serviceNameSpace = serviceNameSpace;
51+
return this;
52+
}
53+
54+
public Builder errorMessage(String errorMessage) {
55+
this.errorMessage = errorMessage;
56+
return this;
57+
}
58+
59+
public FieldResolverGraphQLError build() {
60+
String msgTemplate = "%s. "
61+
+ " fieldName=%s, "
62+
+ " parentTypeName=%s, "
63+
+ " resolverDirectiveDefinition=%s,"
64+
+ " serviceNameSpace=%s";
65+
66+
this.message = String.format(msgTemplate, errorMessage, fieldName, parentTypeName,
67+
resolverDirectiveDefinition, serviceNameSpace);
68+
this.errorClassification = ErrorType.DataFetchingException;
69+
return new FieldResolverGraphQLError(this);
70+
}
71+
}
72+
73+
}

src/main/java/com/intuit/graphql/orchestrator/fieldresolver/QueryOperationFactory.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
import graphql.language.OperationDefinition.Operation;
55
import graphql.language.SelectionSet;
66

7-
import java.util.function.Supplier;
8-
97
public class QueryOperationFactory {
108

11-
public OperationDefinition create(String operationName, Supplier<SelectionSet> selectionSetSupplier) {
9+
public OperationDefinition create(String operationName, SelectionSet selectionSet) {
1210
return OperationDefinition.newOperationDefinition()
1311
.name(operationName)
14-
.selectionSet(selectionSetSupplier.get())
12+
.selectionSet(selectionSet)
1513
.operation(Operation.QUERY)
1614
.build();
1715

src/main/java/com/intuit/graphql/orchestrator/xtext/FieldContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.intuit.graphql.orchestrator.xtext;
22

3+
import graphql.schema.FieldCoordinates;
34
import java.util.Objects;
45
import lombok.Getter;
56

@@ -8,10 +9,12 @@ public class FieldContext {
89

910
private final String parentType;
1011
private final String fieldName;
12+
private final FieldCoordinates fieldCoordinates;
1113

1214
public FieldContext(String parentType, String fieldName) {
1315
this.parentType = parentType;
1416
this.fieldName = fieldName;
17+
this.fieldCoordinates = FieldCoordinates.coordinates(parentType, fieldName);
1518
}
1619

1720
@Override

src/test/groovy/com/intuit/graphql/orchestrator/integration/FieldResolverDirectiveNestedSpec.groovy renamed to src/test/groovy/com/intuit/graphql/orchestrator/integration/FieldResolverDirectiveTargetIsNestedFieldSpec.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import graphql.ExecutionResult
88
import helpers.BaseIntegrationTestSpecification
99
import spock.lang.Subject
1010

11-
class FieldResolverDirectiveNestedSpec extends BaseIntegrationTestSpecification {
11+
class FieldResolverDirectiveTargetIsNestedFieldSpec extends BaseIntegrationTestSpecification {
1212
def PERSON_DOWNSTREAM_QUERY = "query Get_Person {person {id} person {name}}";
1313
def BOOK_DOWNSTREAM_QUERY = "query Get_Person {person {book {id name author {lastName}}}}";
1414
def PETS_DOWNSTREAM_QUERY = "query Get_Person {person {pets {name}}}";
@@ -62,7 +62,7 @@ class FieldResolverDirectiveNestedSpec extends BaseIntegrationTestSpecification
6262
.build();
6363
}
6464

65-
def "testFieldResolverInNestedField"() {
65+
def "FieldResolver target is a nested field"() {
6666
given:
6767
specUnderTest = createGraphQLOrchestrator([mockPersonService, mockBookService, mockPetsService]);
6868

0 commit comments

Comments
 (0)