Skip to content

Commit 5d487ea

Browse files
committed
graphql-java#377 - have the ability to know and capture all fields in a data fetcher
1 parent 3c0d053 commit 5d487ea

File tree

10 files changed

+246
-41
lines changed

10 files changed

+246
-41
lines changed

src/main/java/graphql/execution/Execution.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ private ExecutionResult executeOperation(
7878
return new ExecutionResultImpl(Collections.singletonList(new MutationNotSupportedError()));
7979
}
8080

81-
Map<String, List<Field>> fields = new LinkedHashMap<>();
82-
fieldCollector.collectFields(executionContext, operationRootType, operationDefinition.getSelectionSet(), new ArrayList<>(), fields);
83-
81+
Map<String, List<Field>> fields = fieldCollector.collectFields(executionContext,operationRootType,operationDefinition.getSelectionSet());
8482

8583
ExecutionParameters parameters = newParameters()
8684
.typeInfo(newTypeInfo().type(operationRootType))

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,7 @@ protected ExecutionResult completeValue(ExecutionContext executionContext, Execu
140140
resolvedType = (GraphQLObjectType) fieldType;
141141
}
142142

143-
Map<String, List<Field>> subFields = new LinkedHashMap<>();
144-
List<String> visitedFragments = new ArrayList<>();
145-
for (Field field : fields) {
146-
if (field.getSelectionSet() == null) continue;
147-
fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields);
148-
}
143+
Map<String, List<Field>> subFields = fieldCollector.collectFields(executionContext,resolvedType,fields);
149144

150145
ExecutionParameters newParameters = ExecutionParameters.newParameters()
151146
.typeInfo(typeInfo.asType(resolvedType))

src/main/java/graphql/execution/FieldCollector.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
package graphql.execution;
22

33

4-
import graphql.language.*;
5-
import graphql.schema.*;
4+
import graphql.language.Field;
5+
import graphql.language.FragmentDefinition;
6+
import graphql.language.FragmentSpread;
7+
import graphql.language.InlineFragment;
8+
import graphql.language.Selection;
9+
import graphql.language.SelectionSet;
10+
import graphql.schema.GraphQLInterfaceType;
11+
import graphql.schema.GraphQLObjectType;
12+
import graphql.schema.GraphQLType;
13+
import graphql.schema.GraphQLUnionType;
14+
import graphql.schema.SchemaUtil;
615

716
import java.util.ArrayList;
17+
import java.util.LinkedHashMap;
818
import java.util.List;
919
import java.util.Map;
1020

1121
import static graphql.execution.TypeFromAST.getTypeFromAST;
1222

23+
/**
24+
* A field collector can iterate over field selection sets and build out the sub fields that have been selected,
25+
* expanding named and inline fragments as it goes.s
26+
*/
1327
public class FieldCollector {
1428

1529
private ConditionalNodes conditionalNodes;
@@ -21,7 +35,45 @@ public FieldCollector() {
2135
}
2236

2337

24-
public void collectFields(ExecutionContext executionContext, GraphQLObjectType type, SelectionSet selectionSet, List<String> visitedFragments, Map<String, List<Field>> fields) {
38+
/**
39+
* Given a list of fields this will collect the sub-field selections and return it as a map
40+
*
41+
* @param executionContext the {@link ExecutionContext} in play
42+
* @param objectType the graphql object type in context
43+
* @param fields the list of fields to collect for
44+
*
45+
* @return a map of the sub field selections
46+
*/
47+
public Map<String, List<Field>> collectFields(ExecutionContext executionContext, GraphQLObjectType objectType, List<Field> fields) {
48+
Map<String, List<Field>> subFields = new LinkedHashMap<>();
49+
List<String> visitedFragments = new ArrayList<>();
50+
for (Field field : fields) {
51+
if (field.getSelectionSet() == null) {
52+
continue;
53+
}
54+
this.collectFields(executionContext, objectType, field.getSelectionSet(), visitedFragments, subFields);
55+
}
56+
return subFields;
57+
}
58+
59+
/**
60+
* Given a selection set this will collect the sub-field selections and return it as a map
61+
*
62+
* @param executionContext the {@link ExecutionContext} in play
63+
* @param objectType the graphql object type in context
64+
* @param selectionSet the selection set to collect on
65+
*
66+
* @return a map of the sub field selections
67+
*/
68+
public Map<String, List<Field>> collectFields(ExecutionContext executionContext, GraphQLObjectType objectType, SelectionSet selectionSet) {
69+
Map<String, List<Field>> subFields = new LinkedHashMap<>();
70+
List<String> visitedFragments = new ArrayList<>();
71+
this.collectFields(executionContext, objectType, selectionSet, visitedFragments, subFields);
72+
return subFields;
73+
}
74+
75+
76+
private void collectFields(ExecutionContext executionContext, GraphQLObjectType type, SelectionSet selectionSet, List<String> visitedFragments, Map<String, List<Field>> fields) {
2577

2678
for (Selection selection : selectionSet.getSelections()) {
2779
if (selection instanceof Field) {

src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,7 @@ private boolean isNonNull(GraphQLType fieldType) {
187187
private Map<String, List<Field>> getChildFields(ExecutionContext executionContext, GraphQLObjectType resolvedType,
188188
List<Field> fields) {
189189

190-
Map<String, List<Field>> subFields = new LinkedHashMap<>();
191-
List<String> visitedFragments = new ArrayList<>();
192-
for (Field field : fields) {
193-
if (field.getSelectionSet() == null) continue;
194-
fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields);
195-
}
196-
return subFields;
190+
return fieldCollector.collectFields(executionContext,resolvedType,fields);
197191
}
198192

199193
private GraphQLObjectType getGraphQLObjectType(GraphQLType fieldType, Object value) {

src/main/java/graphql/execution/batched/UnbatchedDataFetcher.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,10 @@ public Object get(DataFetchingEnvironment environment) {
3131
DataFetchingEnvironment singleEnv = new DataFetchingEnvironmentImpl(
3232
source,
3333
environment.getArguments(),
34-
environment.getContext(),
3534
environment.getFields(),
3635
environment.getFieldType(),
3736
environment.getParentType(),
38-
environment.getGraphQLSchema(),
39-
environment.getFragmentsByName(),
40-
environment.getExecutionId());
37+
environment.getExecutionContext());
4138
results.add(delegate.get(singleEnv));
4239
}
4340
return results;

src/main/java/graphql/schema/DataFetchingEnvironment.java

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

3+
import graphql.execution.ExecutionContext;
34
import graphql.execution.ExecutionId;
45
import graphql.language.Field;
56
import graphql.language.FragmentDefinition;
@@ -81,4 +82,9 @@ public interface DataFetchingEnvironment {
8182
* @return the {@link ExecutionId} for the current operation
8283
*/
8384
ExecutionId getExecutionId();
85+
86+
/**
87+
* @return the {@link ExecutionContext} for the current operation
88+
*/
89+
ExecutionContext getExecutionContext();
8490
}

src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,21 @@ public class DataFetchingEnvironmentImpl implements DataFetchingEnvironment {
1818
private final GraphQLOutputType fieldType;
1919
private final GraphQLType parentType;
2020
private final GraphQLSchema graphQLSchema;
21-
private Map<String, FragmentDefinition> fragmentsByName;
22-
private ExecutionId executionId;
21+
private final Map<String, FragmentDefinition> fragmentsByName;
22+
private final ExecutionId executionId;
23+
private final ExecutionContext executionContext;
2324

2425
public DataFetchingEnvironmentImpl(Object source, Map<String, Object> arguments, List<Field> fields, GraphQLOutputType fieldType, GraphQLType parentType, ExecutionContext executionContext) {
25-
this(source, arguments, executionContext.getRoot(), fields, fieldType, parentType, executionContext.getGraphQLSchema(), executionContext.getFragmentsByName(), executionContext.getExecutionId());
26-
}
27-
28-
public DataFetchingEnvironmentImpl(Object source, Map<String, Object> arguments, Object context, List<Field> fields, GraphQLOutputType fieldType, GraphQLType parentType, GraphQLSchema graphQLSchema, Map<String, FragmentDefinition> fragmentsByName, ExecutionId executionId) {
2926
this.source = source;
3027
this.arguments = arguments;
31-
this.context = context;
28+
this.context = executionContext.getRoot();
3229
this.fields = fields;
3330
this.fieldType = fieldType;
3431
this.parentType = parentType;
35-
this.graphQLSchema = graphQLSchema;
36-
this.fragmentsByName = fragmentsByName;
37-
this.executionId = executionId;
32+
this.graphQLSchema = executionContext.getGraphQLSchema();
33+
this.fragmentsByName = executionContext.getFragmentsByName();
34+
this.executionId = executionContext.getExecutionId();
35+
this.executionContext = executionContext;
3836
}
3937

4038
@Override
@@ -58,7 +56,7 @@ public <T> T getArgument(String name) {
5856
}
5957

6058
@Override
61-
public <T> T getContext() {
59+
public <T> T getContext() {
6260
return (T) context;
6361
}
6462

@@ -91,4 +89,9 @@ public Map<String, FragmentDefinition> getFragmentsByName() {
9189
public ExecutionId getExecutionId() {
9290
return executionId;
9391
}
92+
93+
@Override
94+
public ExecutionContext getExecutionContext() {
95+
return executionContext;
96+
}
9497
}

src/test/groovy/graphql/DataFetcherTest.groovy

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package graphql
22

3+
import graphql.execution.ExecutionContext
34
import graphql.schema.DataFetchingEnvironmentImpl
45
import graphql.schema.FieldDataFetcher
56
import graphql.schema.GraphQLOutputType
6-
import graphql.schema.GraphQLScalarType
77
import graphql.schema.PropertyDataFetcher
88
import spock.lang.Specification
99

@@ -55,8 +55,8 @@ class DataFetcherTest extends Specification {
5555
dataHolder.setBooleanFieldWithGet(false)
5656
}
5757

58-
private def env(GraphQLOutputType type) {
59-
new DataFetchingEnvironmentImpl(dataHolder, null, null, null, type, null, null, null, null)
58+
def env(GraphQLOutputType type) {
59+
new DataFetchingEnvironmentImpl(dataHolder, null, null, type, null, new ExecutionContext(null, null, null, null, null, null, null, null, null))
6060
}
6161

6262
def "get field value"() {
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package graphql.schema
2+
3+
import graphql.GraphQL
4+
import graphql.StarWarsData
5+
import graphql.execution.FieldCollector
6+
import graphql.language.AstPrinter
7+
import graphql.language.Field
8+
import graphql.schema.idl.RuntimeWiring
9+
import graphql.schema.idl.SchemaCompiler
10+
import graphql.schema.idl.SchemaGenerator
11+
import spock.lang.Specification
12+
13+
import java.util.stream.Collectors
14+
15+
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring
16+
17+
/**
18+
* #377 - this test proves that a data fetcher has enough information to allow sub request
19+
* proxying or fine grained control of the fields in an object that should be returned
20+
*/
21+
class DataFetcherSelectionTest extends Specification {
22+
23+
GraphQLSchema load(String fileName, RuntimeWiring wiring) {
24+
def stream = getClass().getClassLoader().getResourceAsStream(fileName)
25+
26+
def typeRegistry = new SchemaCompiler().compile(new InputStreamReader(stream))
27+
def schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring)
28+
schema
29+
}
30+
31+
class SelectionCapturingDataFetcher implements DataFetcher {
32+
final DataFetcher delegate
33+
final FieldCollector fieldCollector
34+
final List<String> captureList
35+
36+
SelectionCapturingDataFetcher(DataFetcher delegate, List<String> captureList) {
37+
this.delegate = delegate
38+
this.fieldCollector = new FieldCollector()
39+
this.captureList = captureList
40+
}
41+
42+
@Override
43+
Object get(DataFetchingEnvironment environment) {
44+
45+
//
46+
// we capture the inner field selections of the current field that is being fetched
47+
// this would allow proxying or really fine grained controlled object retrieval
48+
// if one was so included
49+
//
50+
def objectType = environment.getFieldType() as GraphQLObjectType
51+
def collectionResult = fieldCollector.collectFields(environment.executionContext, objectType, environment.fields)
52+
53+
String subselection = captureSubSelection(collectionResult)
54+
captureList.add(subselection)
55+
56+
return delegate.get(environment)
57+
}
58+
59+
String captureSubSelection(Map<String, List<Field>> fields) {
60+
return fields.values().stream().map({ f -> captureFields(f) }).collect(Collectors.joining("\n"))
61+
}
62+
63+
String captureFields(List<Field> fields) {
64+
return fields.stream().map({ f -> AstPrinter.printAst(f) }).collect(Collectors.joining("\n"))
65+
}
66+
}
67+
68+
// side effect captured here
69+
List<String> captureList = new ArrayList<String>()
70+
71+
SelectionCapturingDataFetcher captureSelection(DataFetcher delegate) {
72+
return new SelectionCapturingDataFetcher(delegate, captureList)
73+
}
74+
75+
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
76+
.type(newTypeWiring("QueryType")
77+
.dataFetchers(
78+
[
79+
"hero" : captureSelection(new StaticDataFetcher(StarWarsData.getArtoo())),
80+
"human": captureSelection(StarWarsData.getHumanDataFetcher()),
81+
"droid": captureSelection(StarWarsData.getDroidDataFetcher())
82+
])
83+
)
84+
.type(newTypeWiring("Human")
85+
.dataFetcher("friends", captureSelection(StarWarsData.getFriendsDataFetcher()))
86+
)
87+
.type(newTypeWiring("Droid")
88+
.dataFetcher("friends", captureSelection(StarWarsData.getFriendsDataFetcher()))
89+
)
90+
91+
.type(newTypeWiring("Character")
92+
.typeResolver(StarWarsData.getCharacterTypeResolver())
93+
)
94+
.build()
95+
96+
def executableStarWarsSchema = load("starWarsSchema.graphqls", wiring)
97+
98+
def "field selection can be captured via data environment"() {
99+
100+
captureList.clear()
101+
102+
def query = """
103+
query CAPTURED_VIA_DF {
104+
105+
luke: human(id: "1000") {
106+
...HumanFragment # this is a named fragment
107+
homePlanet
108+
}
109+
110+
leia: human(id: "1003") {
111+
... on Character { # this is an inline fragment
112+
id
113+
friends {
114+
name
115+
}
116+
}
117+
appearsIn
118+
}
119+
}
120+
fragment HumanFragment on Human {
121+
name
122+
...FriendsAndFriendsFragment
123+
124+
}
125+
126+
fragment FriendsAndFriendsFragment on Character {
127+
friends {
128+
name
129+
friends {
130+
name
131+
}
132+
}
133+
}
134+
135+
"""
136+
137+
138+
expect:
139+
when:
140+
GraphQL.newGraphQL(executableStarWarsSchema).build().execute(query).data
141+
142+
then:
143+
144+
captureList == [
145+
// luke part
146+
"name\n" +
147+
"friends {\n" +
148+
" name\n" +
149+
" friends {\n" +
150+
" name\n" +
151+
" }\n" +
152+
"}\n" +
153+
"homePlanet",
154+
// leia part
155+
"id\n" +
156+
"friends {\n" +
157+
" name\n" +
158+
"}\n" +
159+
"appearsIn"]
160+
161+
}
162+
}

0 commit comments

Comments
 (0)