Skip to content

Commit 7a83e7b

Browse files
anttipdgriffon
andauthored
Add support for partial updates (#310)
* Add support for Optional parameters * Apply suggestion from @dgriffon Co-authored-by: David Griffon <[email protected]> * Apply suggestion from @dgriffon Co-authored-by: David Griffon <[email protected]> --------- Co-authored-by: David Griffon <[email protected]>
1 parent 1eed74b commit 7a83e7b

File tree

2 files changed

+233
-7
lines changed

2 files changed

+233
-7
lines changed

src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* you may not use this file except in compliance with the License.
44
* You may obtain a copy of the License at
55
*
6-
* http://www.apache.org/licenses/LICENSE-2.0
6+
* http://www.apache.org/licenses/LICENSE-2.0
77
*
88
* Unless required by applicable law or agreed to in writing, software
99
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,10 +20,7 @@
2020
import graphql.schema.*;
2121

2222
import java.lang.reflect.*;
23-
import java.util.ArrayList;
24-
import java.util.Arrays;
25-
import java.util.List;
26-
import java.util.Map;
23+
import java.util.*;
2724

2825
import static graphql.annotations.processor.util.NamingKit.toGraphqlName;
2926
import static graphql.annotations.processor.util.PrefixesUtil.addPrefixToPropertyName;
@@ -120,6 +117,11 @@ private Object[] invocationArgs(DataFetchingEnvironment environment, ProcessingE
120117

121118
private Object buildArg(Type p, GraphQLType graphQLType, Object arg) {
122119
if (arg == null) {
120+
// for Optional parameters null should be returned as Optional.empty() to show a request for a null value
121+
// and not including the parameter in the query at all should be returned as null to show "undefined" value / not set
122+
if (p instanceof ParameterizedType && ((ParameterizedType) p).getRawType() == Optional.class) {
123+
return Optional.empty();
124+
}
123125
return null;
124126
}
125127
if (graphQLType instanceof graphql.schema.GraphQLNonNull) {
@@ -136,7 +138,13 @@ private Object buildArg(Type p, GraphQLType graphQLType, Object arg) {
136138
Map map = (Map) arg;
137139
for (Parameter parameter : parameters) {
138140
String name = toGraphqlName(parameter.getAnnotation(GraphQLName.class) != null ? parameter.getAnnotation(GraphQLName.class).value() : parameter.getName());
139-
objects.add(buildArg(parameter.getParameterizedType(), ((GraphQLInputObjectType) graphQLType).getField(name).getType(), map.get(name)));
141+
// There is a difference between not having a parameter in the query and having it with a null value
142+
// If the value is not given, it will always be null, but if the value is given as null and the parameter is optional, it will be Optional.empty()
143+
if (!map.containsKey(name)) {
144+
objects.add(null);
145+
} else {
146+
objects.add(buildArg(parameter.getParameterizedType(), ((GraphQLInputObjectType) graphQLType).getField(name).getType(), map.get(name)));
147+
}
140148
}
141149
return constructNewInstance(constructor, objects.toArray(new Object[objects.size()]));
142150
}
@@ -148,8 +156,19 @@ private Object buildArg(Type p, GraphQLType graphQLType, Object arg) {
148156
for (Object item : ((List) arg)) {
149157
list.add(buildArg(subType, wrappedType, item));
150158
}
151-
159+
// add Optional wrapper if needed
160+
if (((ParameterizedType) p).getRawType() == Optional.class) {
161+
return Optional.of(list);
162+
}
152163
return list;
164+
} else if (p instanceof ParameterizedType) {
165+
Type subType = ((ParameterizedType) p).getActualTypeArguments()[0];
166+
Object val = buildArg(subType, graphQLType, arg);
167+
// add Optional wrapper if needed
168+
if (((ParameterizedType) p).getRawType() == Optional.class) {
169+
return Optional.ofNullable(val);
170+
}
171+
return val;
153172
} else {
154173
return arg;
155174
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
*/
13+
package graphql.annotations;
14+
15+
import graphql.ExecutionResult;
16+
import graphql.GraphQL;
17+
import graphql.annotations.annotationTypes.GraphQLField;
18+
import graphql.annotations.annotationTypes.GraphQLName;
19+
import graphql.schema.GraphQLSchema;
20+
import org.testng.annotations.Test;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.stream.Collectors;
26+
27+
import static graphql.annotations.AnnotationsSchemaCreator.newAnnotationsSchema;
28+
import static org.testng.Assert.assertEquals;
29+
import static org.testng.Assert.assertTrue;
30+
31+
@SuppressWarnings("unchecked")
32+
public class GraphQLInputOptionalityTest {
33+
34+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
35+
public static class SingleOptionalField {
36+
37+
public SingleOptionalField(@GraphQLName("optionalField") Optional<String> one) {
38+
this.optionalField = one;
39+
}
40+
41+
@GraphQLField
42+
public Optional<String> optionalField;
43+
44+
public String toString() {
45+
return "SingleOptionalField{" +
46+
"optionalField=" + optionalField +
47+
'}';
48+
}
49+
}
50+
51+
public static class QuerySingleOptionalField {
52+
53+
@SuppressWarnings({"unused"})
54+
@GraphQLField
55+
public String getSingleOptionalField(@GraphQLName("field") SingleOptionalField field) {
56+
return field.toString();
57+
}
58+
}
59+
60+
61+
62+
@Test
63+
public void testQueryWithSingleOptionalField() {
64+
String query = "{ getSingleOptionalField(field: {optionalField:\"a\"}) }";
65+
runTest(new QuerySingleOptionalField(), query, "getSingleOptionalField", "SingleOptionalField{optionalField=Optional[a]}");
66+
}
67+
@Test
68+
public void testQueryWithSingleOptionalFieldUndefined() {
69+
String query = "{ getSingleOptionalField(field: {}) }";
70+
runTest(new QuerySingleOptionalField(), query, "getSingleOptionalField", "SingleOptionalField{optionalField=null}");
71+
}
72+
@Test
73+
public void testQueryWithSingleOptionalFieldNull() {
74+
String query = "{ getSingleOptionalField(field: {optionalField:null}) }";
75+
runTest(new QuerySingleOptionalField(), query, "getSingleOptionalField", "SingleOptionalField{optionalField=Optional.empty}");
76+
}
77+
78+
79+
80+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
81+
public static class OptionalAndRequiredFields {
82+
83+
public OptionalAndRequiredFields(@GraphQLName("optionalField") Optional<String> one, @GraphQLName("requiredField") String two) {
84+
this.optionalField = one;
85+
this.requiredField = two;
86+
}
87+
88+
@GraphQLField
89+
public Optional<String> optionalField;
90+
91+
@GraphQLField
92+
private final String requiredField;
93+
94+
public String toString() {
95+
return "OptionalAndRequiredFields{" +
96+
"optionalField=" + optionalField +
97+
", requiredField=" + requiredField +
98+
'}';
99+
}
100+
}
101+
102+
public static class QueryOptionalAndRequiredFields {
103+
@SuppressWarnings({"unused"})
104+
@GraphQLField
105+
public String getOptionalAndRequiredFields(@GraphQLName("fields") OptionalAndRequiredFields fields) {
106+
return fields.toString();
107+
}
108+
}
109+
110+
@Test
111+
public void testQueryWithRequiredField() {
112+
String query = "{ getOptionalAndRequiredFields(fields: {requiredField:\"a\"}) }";
113+
runTest(new QueryOptionalAndRequiredFields(), query, "getOptionalAndRequiredFields", "OptionalAndRequiredFields{optionalField=null, requiredField=a}");
114+
}
115+
@Test
116+
public void testQueryWithRequiredFieldUndefined() {
117+
String query = "{ getOptionalAndRequiredFields(fields: {}) }";
118+
runTest(new QueryOptionalAndRequiredFields(), query, "getOptionalAndRequiredFields", "OptionalAndRequiredFields{optionalField=null, requiredField=null}");
119+
}
120+
@Test
121+
public void testQueryWithRequiredFieldNull() {
122+
String query = "{ getOptionalAndRequiredFields(fields: {requiredField:null}) }";
123+
runTest(new QueryOptionalAndRequiredFields(), query, "getOptionalAndRequiredFields", "OptionalAndRequiredFields{optionalField=null, requiredField=null}");
124+
}
125+
126+
public static class QueryListOptionalAndRequiredFields {
127+
128+
@SuppressWarnings({"unused", "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
129+
@GraphQLField
130+
public String getListOfOptionalAndRequiredFields(@GraphQLName("fieldsList") Optional<List<OptionalAndRequiredFields>> fieldsList) {
131+
return fieldsList == null ? "was null" : (fieldsList.map(list -> list.stream().collect(Collectors.toList()).toString()).orElse("was empty"));
132+
}
133+
}
134+
135+
@Test
136+
public void testQueryListOptionalAndRequiredFields() {
137+
String query = "{ getListOfOptionalAndRequiredFields }";
138+
runTest(new QueryListOptionalAndRequiredFields(), query, "getListOfOptionalAndRequiredFields", "was null");
139+
}
140+
@Test
141+
public void testQueryListOptionalAndRequiredFieldsNullInList() {
142+
String query = "{ getListOfOptionalAndRequiredFields(fieldsList: [{optionalField:\"a\"}, null, {requiredField:\"b\"}, {}]) }";
143+
String expected = "[OptionalAndRequiredFields{optionalField=Optional[a], requiredField=null}, null, OptionalAndRequiredFields{optionalField=null, requiredField=b}, OptionalAndRequiredFields{optionalField=null, requiredField=null}]";
144+
runTest(new QueryListOptionalAndRequiredFields(), query, "getListOfOptionalAndRequiredFields", expected);
145+
}
146+
147+
public static class QueryOptionalList {
148+
@SuppressWarnings({"unused", "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
149+
@GraphQLField
150+
public String list(@GraphQLName("options") Optional<List<String>> options) {
151+
return options == null ? "was null" : (options.map(anotherCodes -> anotherCodes.stream().reduce("", (a, b) -> a + b)).orElseThrow());
152+
}
153+
}
154+
155+
@Test
156+
public void testQueryWithOptionalList() {
157+
String query = "{ list(options: [\"a\", \"b\", \"c\"]) }";
158+
runTest(new QueryOptionalList(), query, "list", "abc");
159+
}
160+
161+
@Test
162+
public void testQueryWithoutList() {
163+
String query = "{ list }";
164+
runTest(new QueryOptionalList(), query, "list", "was null");
165+
}
166+
167+
@Test
168+
public void testQueryWithEmptyList() {
169+
String query = "{ list(options:[]) }";
170+
runTest(new QueryOptionalList(), query, "list", "");
171+
GraphQLSchema schema = newAnnotationsSchema().query(QueryOptionalList.class).build();
172+
}
173+
174+
175+
public static class OptionalListInConstructor{
176+
@SuppressWarnings({"OptionalUsedAsFieldOrParameterType"})
177+
@GraphQLField
178+
public Optional<List<String>> listOfStrings;
179+
180+
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
181+
public OptionalListInConstructor(@GraphQLName("listOfStrings") Optional<List<String>> listOfStrings) {
182+
this.listOfStrings = listOfStrings;
183+
}
184+
}
185+
186+
public static class QueryOptionalListInConstructor{
187+
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
188+
@GraphQLField
189+
public String getOptionalListInConstructor(@GraphQLName("listOfLists") Optional<List<OptionalListInConstructor>> listOfLists) {
190+
return listOfLists.map(listOfListUnwrapped -> listOfListUnwrapped.stream().map(list -> "{strings=" + list.listOfStrings + "}").reduce("", (a, b) -> a + b)).orElseThrow();
191+
}
192+
}
193+
194+
@Test
195+
public void testQueryOptionalListInConstructor() {
196+
String query = "{ getOptionalListInConstructor(listOfLists: [{listOfStrings: [\"a\", \"b\", \"c\"]}, {}, {listOfStrings: [\"d\"]}]) }";
197+
runTest(new QueryOptionalListInConstructor(), query, "getOptionalListInConstructor", "{strings=Optional[[a, b, c]]}{strings=null}{strings=Optional[[d]]}");
198+
}
199+
200+
private void runTest(Object queryObject, String query, String field, String expected) {
201+
GraphQLSchema schema = newAnnotationsSchema().query(queryObject.getClass()).build();
202+
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
203+
ExecutionResult result = graphQL.execute(GraphQLHelper.createExecutionInput(query, queryObject ));
204+
assertTrue(result.getErrors().isEmpty(), result.getErrors().toString());
205+
assertEquals(((Map<String, String>) result.getData()).get(field), expected);
206+
}
207+
}

0 commit comments

Comments
 (0)