Skip to content

Commit 325b1ec

Browse files
committed
Add support for incremental directives (@defer,@stream)
1 parent 1b69c87 commit 325b1ec

16 files changed

+343
-44
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ PROJECT_LICENSE=MIT
77
PROJECT_LICENSE_URL=https://github.com/graphql-java-kickstart/spring-java-servlet/blob/master/LICENSE.md
88
PROJECT_DEV_ID=oliemansm
99
PROJECT_DEV_NAME=Michiel Oliemans
10-
LIB_GRAPHQL_JAVA_VER=22.3
10+
LIB_GRAPHQL_JAVA_VER=25.0
1111
LIB_JACKSON_VER=2.17.2
1212
LIB_SLF4J_VER=2.0.16
1313
LIB_LOMBOK_VER=1.18.34

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/DecoratedExecutionResult.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
22

33
import graphql.ExecutionResult;
44
import graphql.GraphQLError;
5+
import graphql.incremental.IncrementalExecutionResult;
56
import java.util.List;
67
import java.util.Map;
78
import lombok.RequiredArgsConstructor;
89
import org.reactivestreams.Publisher;
910

1011
@RequiredArgsConstructor
11-
class DecoratedExecutionResult implements ExecutionResult {
12+
public class DecoratedExecutionResult implements ExecutionResult {
1213

1314
private final ExecutionResult result;
1415

1516
boolean isAsynchronous() {
1617
return result.getData() instanceof Publisher;
1718
}
1819

20+
boolean isIncremental() {
21+
return result instanceof IncrementalExecutionResult;
22+
}
23+
24+
public IncrementalExecutionResult asIncrementalExecutionResult() {
25+
return (IncrementalExecutionResult) result;
26+
}
27+
1928
@Override
2029
public List<GraphQLError> getErrors() {
2130
return result.getErrors();

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLBatchedQueryResult.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public boolean isBatched() {
1919
public boolean isAsynchronous() {
2020
return false;
2121
}
22+
23+
@Override
24+
public boolean isIncremental() {
25+
return false;
26+
}
2227
}

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLErrorQueryResult.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public boolean isAsynchronous() {
2020
return false;
2121
}
2222

23+
@Override
24+
public boolean isIncremental() {
25+
return false;
26+
}
27+
2328
@Override
2429
public boolean isError() {
2530
return true;

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLObjectMapper.java

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import graphql.ExecutionResult;
1010
import graphql.ExecutionResultImpl;
1111
import graphql.GraphQLError;
12+
import graphql.incremental.DelayedIncrementalPartialResult;
13+
import graphql.incremental.IncrementalPayload;
1214
import graphql.kickstart.execution.config.ConfiguringObjectMapperProvider;
1315
import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer;
1416
import graphql.kickstart.execution.config.ObjectMapperProvider;
@@ -18,6 +20,7 @@
1820
import java.io.InputStream;
1921
import java.io.Writer;
2022
import java.util.ArrayList;
23+
import java.util.HashMap;
2124
import java.util.LinkedHashMap;
2225
import java.util.List;
2326
import java.util.Map;
@@ -118,51 +121,70 @@ public byte[] serializeResultAsBytes(ExecutionResult executionResult) {
118121
return getJacksonMapper().writeValueAsBytes(createResultFromExecutionResult(executionResult));
119122
}
120123

124+
@SneakyThrows
125+
public byte[] serializeDelayedIncrementalResultsAsBytes(DelayedIncrementalPartialResult delayedIncrementalPartialResult) {
126+
return getJacksonMapper().writeValueAsBytes(createResultFromDelayedIncrementalPayloadResult(delayedIncrementalPartialResult));
127+
}
128+
121129
public boolean areErrorsPresent(ExecutionResult executionResult) {
122130
return graphQLErrorHandlerSupplier.get().errorsPresent(executionResult.getErrors());
123131
}
124132

125-
public ExecutionResult sanitizeErrors(ExecutionResult executionResult) {
126-
Object data = executionResult.getData();
133+
public boolean areExtensionsPresent(ExecutionResult executionResult) {
127134
Map<Object, Object> extensions = executionResult.getExtensions();
128-
List<GraphQLError> errors = executionResult.getErrors();
135+
return extensions != null && !extensions.isEmpty();
136+
}
129137

138+
public ExecutionResult sanitizeErrors(ExecutionResult executionResult) {
130139
GraphQLErrorHandler errorHandler = graphQLErrorHandlerSupplier.get();
131-
if (errorHandler.errorsPresent(errors)) {
132-
errors = errorHandler.processErrors(errors);
133-
} else {
134-
errors = null;
135-
}
136-
return new ExecutionResultImpl(data, errors, extensions);
140+
return executionResult.transform(er -> {
141+
List<GraphQLError> errors = executionResult.getErrors();
142+
if (errorHandler.errorsPresent(errors)) {
143+
errors = errorHandler.processErrors(errors);
144+
} else {
145+
errors = List.of();
146+
}
147+
er.errors(errors);
148+
});
149+
}
150+
151+
public DelayedIncrementalPartialResult sanitizeErrors(DelayedIncrementalPartialResult delayedIncrementalPartialResult) {
152+
return delayedIncrementalPartialResult;
137153
}
138154

139155
public Map<String, Object> createResultFromExecutionResult(ExecutionResult executionResult) {
140156
ExecutionResult sanitizedExecutionResult = sanitizeErrors(executionResult);
141157
return convertSanitizedExecutionResult(sanitizedExecutionResult);
142158
}
143159

160+
public Map<String, Object> createResultFromDelayedIncrementalPayloadResult(DelayedIncrementalPartialResult delayedIncrementalPartialResult) {
161+
DelayedIncrementalPartialResult sanitizedDelayedIncrementalPartialResult = sanitizeErrors(delayedIncrementalPartialResult);
162+
return convertSanitizedDelayedIncrementalPartialResult(sanitizedDelayedIncrementalPartialResult);
163+
}
164+
144165
public Map<String, Object> convertSanitizedExecutionResult(ExecutionResult executionResult) {
145166
return convertSanitizedExecutionResult(executionResult, true);
146167
}
147168

169+
public Map<String, Object> convertSanitizedDelayedIncrementalPartialResult(
170+
DelayedIncrementalPartialResult delayedIncrementalPartialResult) {
171+
return delayedIncrementalPartialResult.toSpecification();
172+
}
173+
148174
public Map<String, Object> convertSanitizedExecutionResult(
149175
ExecutionResult executionResult, boolean includeData) {
150-
final Map<String, Object> result = new LinkedHashMap<>();
151-
152-
if (areErrorsPresent(executionResult)) {
153-
result.put(
154-
"errors",
155-
executionResult.getErrors().stream()
156-
.map(GraphQLError::toSpecification)
157-
.collect(toList()));
176+
final Map<String, Object> result = new HashMap<>(executionResult.toSpecification());
177+
178+
if (!areErrorsPresent(executionResult)) {
179+
result.remove("errors");
158180
}
159181

160-
if (executionResult.getExtensions() != null && !executionResult.getExtensions().isEmpty()) {
161-
result.put("extensions", executionResult.getExtensions());
182+
if (!includeData) {
183+
result.remove("data");
162184
}
163185

164-
if (includeData) {
165-
result.put("data", executionResult.getData());
186+
if (!areExtensionsPresent(executionResult)) {
187+
result.remove("extensions");
166188
}
167189

168190
return result;

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLQueryResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ static GraphQLErrorQueryResult createError(int statusCode, String message) {
2323

2424
boolean isAsynchronous();
2525

26+
boolean isIncremental();
27+
2628
default DecoratedExecutionResult getResult() {
2729
return null;
2830
}

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/GraphQLSingleQueryResult.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public boolean isBatched() {
1717
public boolean isAsynchronous() {
1818
return result.isAsynchronous();
1919
}
20+
21+
@Override
22+
public boolean isIncremental() {
23+
return result.isIncremental();
24+
}
2025
}

graphql-java-kickstart/src/main/java/graphql/kickstart/execution/context/DefaultGraphQLContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public void put(Object key, Object value) {
3636
map.put(key, value);
3737
}
3838

39+
public void putAll(Map<Object, Object> values) {
40+
map.putAll(values);
41+
}
42+
3943
@Override
4044
public DataLoaderRegistry getDataLoaderRegistry() {
4145
return dataLoaderRegistry;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package graphql.kickstart.servlet;
2+
3+
import graphql.incremental.DelayedIncrementalPartialResult;
4+
import graphql.kickstart.execution.GraphQLObjectMapper;
5+
import jakarta.servlet.AsyncContext;
6+
import jakarta.servlet.ServletOutputStream;
7+
import jakarta.servlet.ServletResponse;
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.atomic.AtomicReference;
12+
import org.reactivestreams.Subscriber;
13+
import org.reactivestreams.Subscription;
14+
15+
class DelayedIncrementalPartialResultSubscriber implements Subscriber<DelayedIncrementalPartialResult> {
16+
17+
private final AtomicReference<Subscription> subscriptionRef;
18+
private final AsyncContext asyncContext;
19+
private final GraphQLObjectMapper graphQLObjectMapper;
20+
private final CountDownLatch completedLatch = new CountDownLatch(1);
21+
22+
DelayedIncrementalPartialResultSubscriber(
23+
AtomicReference<Subscription> subscriptionRef,
24+
AsyncContext asyncContext,
25+
GraphQLObjectMapper graphQLObjectMapper) {
26+
this.subscriptionRef = subscriptionRef;
27+
this.asyncContext = asyncContext;
28+
this.graphQLObjectMapper = graphQLObjectMapper;
29+
}
30+
31+
@Override
32+
public void onSubscribe(Subscription subscription) {
33+
subscriptionRef.set(subscription);
34+
subscriptionRef.get().request(1);
35+
}
36+
37+
@Override
38+
public void onNext(DelayedIncrementalPartialResult delayedIncrementalPartialResult) {
39+
try {
40+
ServletResponse response = asyncContext.getResponse();
41+
ServletOutputStream outputStream = response.getOutputStream();
42+
outputStream.write(HttpRequestHandler.MULTIPART_BOUNDARY.getBytes(StandardCharsets.UTF_8));
43+
outputStream.write(HttpRequestHandler.MULTIPART_CONTENT_TYPE.getBytes(
44+
StandardCharsets.UTF_8));
45+
byte[] contentBytes = graphQLObjectMapper.serializeDelayedIncrementalResultsAsBytes(delayedIncrementalPartialResult);
46+
outputStream.write(contentBytes);
47+
outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
48+
if (!delayedIncrementalPartialResult.hasNext()) {
49+
outputStream.write(HttpRequestHandler.MULTIPART_BOUNDARY.getBytes(StandardCharsets.UTF_8));
50+
}
51+
outputStream.flush();
52+
subscriptionRef.get().request(1);
53+
} catch (IOException ignored) {
54+
// ignore
55+
}
56+
}
57+
58+
@Override
59+
public void onError(Throwable t) {
60+
asyncContext.complete();
61+
completedLatch.countDown();
62+
}
63+
64+
@Override
65+
public void onComplete() {
66+
asyncContext.complete();
67+
completedLatch.countDown();
68+
}
69+
70+
void await() throws InterruptedException {
71+
completedLatch.await();
72+
}
73+
}

graphql-java-servlet/src/main/java/graphql/kickstart/servlet/HttpRequestHandler.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public interface HttpRequestHandler {
88

99
String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8";
1010
String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8";
11+
String MULTIPART_MIXED = "multipart/mixed; boundary=\"-\"";
12+
String MULTIPART_BOUNDARY = "---\r\n";
13+
String MULTIPART_CONTENT_TYPE = "Content-Type: application/json; charset=UTF-8\r\n\r\n";
1114

1215
int STATUS_OK = 200;
1316
int STATUS_BAD_REQUEST = 400;

0 commit comments

Comments
 (0)