Extract GraphQlRequest and rename "query" to "document"
Extract GraphQlRequest, as a parent of RequestInput, that holds the actual request inputs sent from the client side. RequestInput then adds server-side transport details and ExecutionInput support. Also replace "query" with "document" in GraphQlRequest and in GraphQlClient. Closes gh-310
This commit is contained in:
@@ -68,25 +68,25 @@ public class GraphQlTesterTests {
|
||||
@Test
|
||||
void pathAndValueExist() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute();
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute();
|
||||
|
||||
spec.path("me.name").pathExists().valueExists();
|
||||
spec.path("me.friends").pathExists().valueExists();
|
||||
spec.path("hero").pathDoesNotExist().valueDoesNotExist();
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueIsEmpty() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse("{\"me\": {\"name\":null, \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute();
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute();
|
||||
|
||||
spec.path("me.name").valueIsEmpty();
|
||||
spec.path("me.friends").valueIsEmpty();
|
||||
@@ -95,16 +95,16 @@ public class GraphQlTesterTests {
|
||||
.as("Path does not even exist")
|
||||
.hasMessageContaining("No value at JSON path \"$['data']['hero']");
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesJson() throws Exception {
|
||||
|
||||
String query = "{me {name}}";
|
||||
String document = "{me {name}}";
|
||||
setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute();
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute();
|
||||
|
||||
spec.path("").matchesJson("{\"me\": {\"name\":\"Luke Skywalker\",\"friends\":[]}}");
|
||||
spec.path("me").matchesJson("{\"name\":\"Luke Skywalker\"}");
|
||||
@@ -115,16 +115,16 @@ public class GraphQlTesterTests {
|
||||
.as("Extended fields should fail in strict mode")
|
||||
.hasMessageContaining("Unexpected: name");
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entity() throws Exception {
|
||||
|
||||
String query = "{me {name}}";
|
||||
String document = "{me {name}}";
|
||||
setResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute();
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute();
|
||||
|
||||
MovieCharacter luke = MovieCharacter.create("Luke Skywalker");
|
||||
MovieCharacter han = MovieCharacter.create("Han Solo");
|
||||
@@ -145,13 +145,13 @@ public class GraphQlTesterTests {
|
||||
.entity(new ParameterizedTypeReference<Map<String, MovieCharacter>>() {})
|
||||
.isEqualTo(Collections.singletonMap("me", luke));
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityList() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse("{" +
|
||||
" \"me\":{" +
|
||||
" \"name\":\"Luke Skywalker\","
|
||||
@@ -159,7 +159,7 @@ public class GraphQlTesterTests {
|
||||
" }" +
|
||||
"}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute();
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute();
|
||||
|
||||
MovieCharacter han = MovieCharacter.create("Han Solo");
|
||||
MovieCharacter leia = MovieCharacter.create("Leia Organa");
|
||||
@@ -181,13 +181,13 @@ public class GraphQlTesterTests {
|
||||
.entityList(new ParameterizedTypeReference<MovieCharacter>() {})
|
||||
.containsExactly(han, leia);
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void operationNameAndVariables() throws Exception {
|
||||
|
||||
String query = "query HeroNameAndFriends($episode: Episode) {" +
|
||||
String document = "query HeroNameAndFriends($episode: Episode) {" +
|
||||
" hero(episode: $episode) {" +
|
||||
" name"
|
||||
+ " }" +
|
||||
@@ -195,7 +195,7 @@ public class GraphQlTesterTests {
|
||||
|
||||
setResponse("{\"hero\": {\"name\":\"R2-D2\"}}");
|
||||
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query)
|
||||
GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document)
|
||||
.operationName("HeroNameAndFriends")
|
||||
.variable("episode", "JEDI")
|
||||
.variable("foo", "bar")
|
||||
@@ -205,7 +205,7 @@ public class GraphQlTesterTests {
|
||||
spec.path("hero").entity(MovieCharacter.class).isEqualTo(MovieCharacter.create("R2-D2"));
|
||||
|
||||
RequestInput input = this.inputCaptor.getValue();
|
||||
assertThat(input.getQuery()).contains(query);
|
||||
assertThat(input.getDocument()).contains(document);
|
||||
assertThat(input.getOperationName()).isEqualTo("HeroNameAndFriends");
|
||||
assertThat(input.getVariables()).hasSize(3);
|
||||
assertThat(input.getVariables()).containsEntry("episode", "JEDI");
|
||||
@@ -216,55 +216,55 @@ public class GraphQlTesterTests {
|
||||
@Test
|
||||
void errorsCheckedOnExecuteAndVerify() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build());
|
||||
|
||||
assertThatThrownBy(() -> this.graphQlTester.query(query).executeAndVerify())
|
||||
assertThatThrownBy(() -> this.graphQlTester.query(document).executeAndVerify())
|
||||
.hasMessageContaining("Response has 1 unexpected error(s).");
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsCheckedOnTraverse() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build());
|
||||
|
||||
assertThatThrownBy(() -> this.graphQlTester.query(query).execute().path("me"))
|
||||
assertThatThrownBy(() -> this.graphQlTester.query(document).execute().path("me"))
|
||||
.hasMessageContaining("Response has 1 unexpected error(s).");
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsPartiallyFiltered() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(
|
||||
GraphqlErrorBuilder.newError().message("some error").build(),
|
||||
GraphqlErrorBuilder.newError().message("some other error").build());
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
this.graphQlTester.query(query)
|
||||
this.graphQlTester.query(document)
|
||||
.execute()
|
||||
.errors()
|
||||
.filter((error) -> error.getMessage().equals("some error"))
|
||||
.verify())
|
||||
.hasMessageContaining("Response has 1 unexpected error(s) of 2 total.");
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsFiltered() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(
|
||||
GraphqlErrorBuilder.newError().message("some error").build(),
|
||||
GraphqlErrorBuilder.newError().message("some other error").build());
|
||||
|
||||
this.graphQlTester.query(query)
|
||||
this.graphQlTester.query(document)
|
||||
.execute()
|
||||
.errors()
|
||||
.filter((error) -> error.getMessage().startsWith("some "))
|
||||
@@ -272,13 +272,13 @@ public class GraphQlTesterTests {
|
||||
.path("me")
|
||||
.pathDoesNotExist();
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsFilteredGlobally() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(
|
||||
GraphqlErrorBuilder.newError().message("some error").build(),
|
||||
GraphqlErrorBuilder.newError().message("some other error").build());
|
||||
@@ -286,25 +286,25 @@ public class GraphQlTesterTests {
|
||||
GraphQlTester.builder(this.service)
|
||||
.errorFilter((error) -> error.getMessage().startsWith("some "))
|
||||
.build()
|
||||
.query(query)
|
||||
.query(document)
|
||||
.execute()
|
||||
.errors()
|
||||
.verify()
|
||||
.path("me")
|
||||
.pathDoesNotExist();
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsExpected() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(
|
||||
GraphqlErrorBuilder.newError().message("some error").build(),
|
||||
GraphqlErrorBuilder.newError().message("some other error").build());
|
||||
|
||||
this.graphQlTester.query(query)
|
||||
this.graphQlTester.query(document)
|
||||
.execute()
|
||||
.errors()
|
||||
.expect((error) -> error.getMessage().startsWith("some "))
|
||||
@@ -312,19 +312,19 @@ public class GraphQlTesterTests {
|
||||
.path("me")
|
||||
.pathDoesNotExist();
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsExpectedButNotFound() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(
|
||||
GraphqlErrorBuilder.newError().message("some error").build(),
|
||||
GraphqlErrorBuilder.newError().message("some other error").build());
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
this.graphQlTester.query(query)
|
||||
this.graphQlTester.query(document)
|
||||
.execute()
|
||||
.errors().expect((error) -> error.getMessage().startsWith("another ")))
|
||||
.hasMessageStartingWith("No matching errors.");
|
||||
@@ -333,13 +333,13 @@ public class GraphQlTesterTests {
|
||||
@Test
|
||||
void errorsConsumed() throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
String document = "{me {name, friends}}";
|
||||
setResponse(GraphqlErrorBuilder.newError()
|
||||
.message("Invalid query")
|
||||
.location(new SourceLocation(1, 2))
|
||||
.build());
|
||||
|
||||
this.graphQlTester.query(query)
|
||||
this.graphQlTester.query(document)
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy((errors) -> {
|
||||
@@ -352,7 +352,7 @@ public class GraphQlTesterTests {
|
||||
.path("me")
|
||||
.pathDoesNotExist();
|
||||
|
||||
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
|
||||
assertThat(this.inputCaptor.getValue().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
private void setResponse(String data) throws Exception {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -91,7 +91,7 @@ public class WebGraphQlTesterTests {
|
||||
spec.path("me.name").entity(String.class).isEqualTo("Luke Skywalker");
|
||||
|
||||
setup.verifyRequest((input) -> {
|
||||
assertThat(input.getQuery()).contains(query);
|
||||
assertThat(input.getDocument()).contains(query);
|
||||
assertThat(input.getHeaders().get("myHeader1")).containsExactly("myValue1a", "myValue1b");
|
||||
assertThat(input.getHeaders().getFirst("myHeader2")).isEqualTo("myValue2");
|
||||
});
|
||||
@@ -116,7 +116,7 @@ public class WebGraphQlTesterTests {
|
||||
spec.path("me.name").entity(String.class).isEqualTo("Luke Skywalker");
|
||||
|
||||
setup.verifyRequest((input) -> {
|
||||
assertThat(input.getQuery()).contains(query);
|
||||
assertThat(input.getDocument()).contains(query);
|
||||
assertThat(input.getHeaders().get("myHeader1")).containsExactly("myValue1a", "myValue1b");
|
||||
assertThat(input.getHeaders().getFirst("myHeader2")).isEqualTo("myValue2");
|
||||
});
|
||||
@@ -143,7 +143,7 @@ public class WebGraphQlTesterTests {
|
||||
.path("me")
|
||||
.pathDoesNotExist();
|
||||
|
||||
setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query));
|
||||
setup.verifyRequest((input) -> assertThat(input.getDocument()).contains(query));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a GraphQL request with the inputs to pass to a GraphQL service
|
||||
* including a {@link #getDocument() document}, {@link #getOperationName()
|
||||
* operationName}, and {@link #getVariables() variables}.
|
||||
*
|
||||
* <p>The request can be turned to a Map via {@link #toMap()} and to be
|
||||
* submitted as JSON over HTTP or WebSocket.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class GraphQlRequest {
|
||||
|
||||
private final String document;
|
||||
|
||||
@Nullable
|
||||
private final String operationName;
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
|
||||
|
||||
/**
|
||||
* Create a request.
|
||||
* @param document textual representation of the operation(s)
|
||||
*/
|
||||
public GraphQlRequest(String document) {
|
||||
this(document, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request with a complete set of inputs.
|
||||
* @param document textual representation of the operation(s)
|
||||
* @param operationName optionally, the name of the operation to execute
|
||||
* @param variables variables by which the operation is parameterized
|
||||
*/
|
||||
public GraphQlRequest(String document, @Nullable String operationName, @Nullable Map<String, Object> variables) {
|
||||
Assert.notNull(document, "'document' is required");
|
||||
this.document = document;
|
||||
this.operationName = operationName;
|
||||
this.variables = ((variables != null) ? variables : Collections.emptyMap());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the GraphQL document which is the textual representation of an
|
||||
* operation (or operations) to perform, including any selection sets and
|
||||
* fragments.
|
||||
*/
|
||||
public String getDocument() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the operation in the {@link #getDocument() document}
|
||||
* to execute, if the document contains multiple operations.
|
||||
*/
|
||||
@Nullable
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return values for variable defined by the operation.
|
||||
*/
|
||||
public Map<String, Object> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the request to a {@link Map} as defined in
|
||||
* <a href="https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md">GraphQL over HTTP</a> and
|
||||
* <a href="https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md">GraphQL over WebSocket</a>:
|
||||
* <table>
|
||||
* <tr><th>Key</th><th>Value</th></tr>
|
||||
* <tr><td>query</td><td>{@link #getDocument() document}</td></tr>
|
||||
* <tr><td>operationName</td><td>{@link #getOperationName() operationName}</td></tr>
|
||||
* <tr><td>variables</td><td>{@link #getVariables() variables}</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>(3);
|
||||
map.put("query", getDocument());
|
||||
if (getOperationName() != null) {
|
||||
map.put("operationName", getOperationName());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getVariables())) {
|
||||
map.put("variables", new LinkedHashMap<>(getVariables()));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "document='" + getDocument() + "'" +
|
||||
((getOperationName() != null) ? ", operationName='" + getOperationName() + "'" : "") +
|
||||
(!CollectionUtils.isEmpty(getVariables()) ? ", variables=" + getVariables() : "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.springframework.graphql;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Strategy to perform a GraphQL request.
|
||||
* Strategy to execute a GraphQL request.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
@@ -27,7 +27,7 @@ import reactor.core.publisher.Mono;
|
||||
public interface GraphQlService {
|
||||
|
||||
/**
|
||||
* Perform the operation and return the result.
|
||||
* Execute the GraphQL request and return the result.
|
||||
* @param input container for GraphQL request input
|
||||
* @return the result from execution
|
||||
*/
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -29,26 +27,22 @@ import graphql.execution.ExecutionId;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Common, server-side representation of GraphQL request input independent of
|
||||
* the underlying transport. This can be converted to {@link ExecutionInput}
|
||||
* via {@link #toExecutionInput()} while the resulting {@code ExecutionInput}
|
||||
* can be customized via {@link #configureExecutionInput(BiFunction)} callbacks.
|
||||
* Extension of {@link GraphQlRequest} for server side handling, adding the
|
||||
* transport (e.g. HTTP or WebSocket handler) assigned {@link #getId() id} and
|
||||
* {@link #getLocale() locale} in the addition to the {@link GraphQlRequest}
|
||||
* inputs.
|
||||
*
|
||||
* <p>{@code RequestInput} supports the initialization of {@link ExecutionInput}
|
||||
* that is passed to {@link graphql.GraphQL}. You can customize that via
|
||||
* {@link #configureExecutionInput(BiFunction)}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class RequestInput {
|
||||
|
||||
private final String query;
|
||||
|
||||
@Nullable
|
||||
private final String operationName;
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
public class RequestInput extends GraphQlRequest {
|
||||
|
||||
@Nullable
|
||||
private final Locale locale;
|
||||
@@ -63,52 +57,60 @@ public class RequestInput {
|
||||
|
||||
/**
|
||||
* Create an instance.
|
||||
* @param query the query, mutation, or subscription for the request
|
||||
* @param operationName an optional, explicit name assigned to the query
|
||||
* @param variables variables by which the query is parameterized
|
||||
* @param locale the locale associated with the request, if any
|
||||
* @param document textual representation of the operation(s)
|
||||
* @param operationName optionally, the name of the operation to execute
|
||||
* @param variables variables by which the query is parameterized
|
||||
* @param locale the locale associated with the request
|
||||
* @param id the request id, to be used as the {@link ExecutionId}
|
||||
*/
|
||||
public RequestInput(
|
||||
String query, @Nullable String operationName, @Nullable Map<String, Object> variables,
|
||||
String document, @Nullable String operationName, @Nullable Map<String, Object> variables,
|
||||
@Nullable Locale locale, String id) {
|
||||
|
||||
Assert.notNull(query, "'query' is required");
|
||||
super(document, operationName, variables);
|
||||
Assert.notNull(id, "'id' is required");
|
||||
this.query = query;
|
||||
this.operationName = operationName;
|
||||
this.variables = ((variables != null) ? variables : Collections.emptyMap());
|
||||
this.locale = locale;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the id for the request selected by the transport handler.
|
||||
* Return the transport assigned id for the request which is then used to set
|
||||
* {@link ExecutionInput.Builder#executionId(ExecutionId) executionId}.
|
||||
* The is initialized as follows:
|
||||
* <ul>
|
||||
* <li>For WebFlux, this is the {@code ServerHttpRequest} id which correlates
|
||||
* to WebFlux log messages. For Reactor Netty, it also correlates to server
|
||||
* log messages.
|
||||
* <li>For Spring MVC, the id is generated via
|
||||
* {@link org.springframework.util.AlternativeJdkIdGenerator}, which is more
|
||||
* efficient than {@code UUID.randomUUID()}.
|
||||
* <li>For WebFlux the id is from the {@code ServerHttpRequest}, which is
|
||||
* useful to correlate to WebFlux log messages.
|
||||
* <li>For WebSocket, the id is from the {@code Subscribe} message of the
|
||||
* GraphQL over WebSocket protocol, which is useful to correlate to
|
||||
* WebSocket messages.
|
||||
* {@link org.springframework.util.AlternativeJdkIdGenerator}, which does
|
||||
* not correlate to anything, but is more efficient than the default
|
||||
* {@link graphql.execution.ExecutionIdProvider} which relies on
|
||||
* {@code UUID.randomUUID()}.
|
||||
* <li>For WebSocket, this is the GraphQL over WebSocket {@code "subscribe"}
|
||||
* message id, which correlates to WebSocket messages.
|
||||
* </ul>
|
||||
* <p> By default, the transport id becomes the
|
||||
* {@link ExecutionInput.Builder#executionId(ExecutionId) executionId} for
|
||||
* the GraphQL request. You can override this via
|
||||
* {@link #executionId(ExecutionId)} or by configuring an
|
||||
* {@link graphql.execution.ExecutionIdProvider} on {@link graphql.GraphQL}.
|
||||
* <p>To override this id, use {@link #executionId(ExecutionId)} or configure
|
||||
* {@link graphql.GraphQL} with an {@link graphql.execution.ExecutionIdProvider}.
|
||||
* @return the request id
|
||||
* @see <a href="https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md">GraphQL over WebSocket Protocol</a>
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@code executionId} configured via {@link #executionId(ExecutionId)}.
|
||||
* Configure the {@link ExecutionId} to set on
|
||||
* {@link ExecutionInput#getExecutionId()}, overriding the transport assigned
|
||||
* {@link #getId() id}.
|
||||
* @param executionId the id to use
|
||||
*/
|
||||
public void executionId(ExecutionId executionId) {
|
||||
Assert.notNull(executionId, "executionId is required");
|
||||
this.executionId = executionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link #executionId(ExecutionId) executionId}.
|
||||
*/
|
||||
@Nullable
|
||||
public ExecutionId getExecutionId() {
|
||||
@@ -116,33 +118,7 @@ public class RequestInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query, mutation, or subscription for the request.
|
||||
* @return the query, a non-empty string.
|
||||
*/
|
||||
public String getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the explicitly assigned name for the query.
|
||||
* @return the operation name or {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return values for variable referenced within the query via $syntax.
|
||||
* @return a map of variables, or an empty map.
|
||||
*/
|
||||
public Map<String, Object> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locale associated with the request.
|
||||
* @return the locale of {@code null}.
|
||||
* Return the transport assigned locale value, if any.
|
||||
*/
|
||||
@Nullable
|
||||
public Locale getLocale() {
|
||||
@@ -150,40 +126,29 @@ public class RequestInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ExecutionId} to use for the GraphQL request, which
|
||||
* is set on the {@link ExecutionInput}. This option overrides the
|
||||
* {@link #getId() id} selected by the transport handler.
|
||||
* @param executionId the execution id to set on the {@link ExecutionInput}.
|
||||
*/
|
||||
public void executionId(ExecutionId executionId) {
|
||||
Assert.notNull(executionId, "executionId should not be null");
|
||||
this.executionId = executionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a consumer to configure the {@link ExecutionInput} used for input to
|
||||
* {@link graphql.GraphQL#executeAsync(ExecutionInput)}. The builder is initially
|
||||
* populated with the values from {@link #getQuery()}, {@link #getOperationName()},
|
||||
* and {@link #getVariables()}.
|
||||
* @param configurer a {@code BiFunction} with the current {@code ExecutionInput} and
|
||||
* a builder to modify it.
|
||||
* Provide a {@code BiFunction} to help initialize the {@link ExecutionInput}
|
||||
* passed to {@link graphql.GraphQL}. The {@code ExecutionInput} is first
|
||||
* pre-populated with values from "this" {@code RequestInput}, and is then
|
||||
* customized with the functions provided here.
|
||||
* @param configurer a {@code BiFunction} that accepts the
|
||||
* {@code ExecutionInput} initialized so far, and a builder to customize it.
|
||||
*/
|
||||
public void configureExecutionInput(BiFunction<ExecutionInput, ExecutionInput.Builder, ExecutionInput> configurer) {
|
||||
this.executionInputConfigurers.add(configurer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link ExecutionInput} for request execution. This is initially
|
||||
* populated from {@link #getQuery()}, {@link #getOperationName()}, and
|
||||
* {@link #getVariables()}, and is then further customized through
|
||||
* {@link #configureExecutionInput(BiFunction)}.
|
||||
* @return the execution input
|
||||
* Create the {@link ExecutionInput} to pass to {@link graphql.GraphQL}.
|
||||
* passed to {@link graphql.GraphQL}. The {@code ExecutionInput} is populated
|
||||
* with values from "this" {@code RequestInput}, and then customized with
|
||||
* functions provided via {@link #configureExecutionInput(BiFunction)}.
|
||||
* @return the resulting {@code ExecutionInput}
|
||||
*/
|
||||
public ExecutionInput toExecutionInput() {
|
||||
ExecutionInput.Builder inputBuilder = ExecutionInput.newExecutionInput()
|
||||
.query(this.query)
|
||||
.operationName(this.operationName)
|
||||
.variables(this.variables)
|
||||
.query(getDocument())
|
||||
.operationName(getOperationName())
|
||||
.variables(getVariables())
|
||||
.locale(this.locale)
|
||||
.executionId(this.executionId != null ? this.executionId : ExecutionId.from(this.id));
|
||||
|
||||
@@ -197,28 +162,9 @@ public class RequestInput {
|
||||
return executionInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map representation of the request input.
|
||||
* @return map representation of the input
|
||||
*/
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>(3);
|
||||
map.put("query", getQuery());
|
||||
if (getOperationName() != null) {
|
||||
map.put("operationName", getOperationName());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getVariables())) {
|
||||
map.put("variables", new LinkedHashMap<>(getVariables()));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Query='" + getQuery() + "'" +
|
||||
((getOperationName() != null) ? ", Operation='" + getOperationName() + "'" : "") +
|
||||
(!CollectionUtils.isEmpty(getVariables()) ? ", Variables=" + getVariables() : "") +
|
||||
(getLocale() != null ? ", Locale=" + getLocale() : "");
|
||||
return super.toString() + (getLocale() != null ? ", Locale=" + getLocale() : "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -45,18 +45,19 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final Configuration jsonPathConfig;
|
||||
|
||||
private final OperationContentLoader operationContentLoader;
|
||||
private final DocumentSource documentSource;
|
||||
|
||||
|
||||
public DefaultGraphQlClient(GraphQlTransport transport, Configuration jsonPathConfig,
|
||||
OperationContentLoader operationContentLoader) {
|
||||
DefaultGraphQlClient(
|
||||
GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource) {
|
||||
|
||||
Assert.notNull(transport, "GraphQlTransport is required");
|
||||
Assert.notNull(jsonPathConfig, "'jsonPathConfig' is required");
|
||||
Assert.notNull(operationContentLoader, "RequestNameResolver is required");
|
||||
Assert.notNull(jsonPathConfig, "Configuration is required");
|
||||
Assert.notNull(documentSource, "DocumentSource is required");
|
||||
|
||||
this.transport = transport;
|
||||
this.jsonPathConfig = jsonPathConfig;
|
||||
this.operationContentLoader = operationContentLoader;
|
||||
this.documentSource = documentSource;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,21 +68,21 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec operation(String operation) {
|
||||
return new DefaultRequestSpec(operation, this.transport, this.jsonPathConfig);
|
||||
public RequestSpec document(String document) {
|
||||
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec loadOperationContent(String key) {
|
||||
String requestContent = this.operationContentLoader.loadOperation(key);
|
||||
Assert.notNull(requestContent, "Failed to load operation content for key: " + key);
|
||||
return new DefaultRequestSpec(requestContent, this.transport, this.jsonPathConfig);
|
||||
public RequestSpec documentName(String name) {
|
||||
String document = this.documentSource.getDocument(name);
|
||||
Assert.notNull(document, "Failed to find document for name: '" + name + "'");
|
||||
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
|
||||
}
|
||||
|
||||
|
||||
private static final class DefaultRequestSpec implements RequestSpec {
|
||||
|
||||
private final String operation;
|
||||
private final String document;
|
||||
|
||||
@Nullable
|
||||
private String operationName;
|
||||
@@ -92,16 +93,16 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final Configuration jsonPathConfig;
|
||||
|
||||
DefaultRequestSpec(String operation, GraphQlTransport transport, Configuration jsonPathConfig) {
|
||||
Assert.hasText(operation, "'operation' is required");
|
||||
this.operation = operation;
|
||||
DefaultRequestSpec(String document, GraphQlTransport transport, Configuration jsonPathConfig) {
|
||||
Assert.hasText(document, "'document' is required");
|
||||
this.document = document;
|
||||
this.transport = transport;
|
||||
this.jsonPathConfig = jsonPathConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequestSpec operationName(@Nullable String name) {
|
||||
this.operationName = name;
|
||||
public DefaultRequestSpec operationName(@Nullable String operationName) {
|
||||
this.operationName = operationName;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -113,18 +114,18 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
@Override
|
||||
public Mono<ResponseSpec> execute() {
|
||||
return this.transport.execute(initRequestInput())
|
||||
return this.transport.execute(createRequest())
|
||||
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ResponseSpec> executeSubscription() {
|
||||
return this.transport.executeSubscription(initRequestInput())
|
||||
return this.transport.executeSubscription(createRequest())
|
||||
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
|
||||
}
|
||||
|
||||
private RequestInput initRequestInput() {
|
||||
return new RequestInput(this.operation, this.operationName, this.variables, null, "1");
|
||||
private GraphQlRequest createRequest() {
|
||||
return new GraphQlRequest(this.document, this.operationName, this.variables);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -132,33 +133,33 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final DocumentContext documentContext;
|
||||
private final DocumentContext jsonPathDocument;
|
||||
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
private DefaultResponseSpec(ExecutionResult result, Configuration jsonPathConfig) {
|
||||
this.documentContext = JsonPath.parse(result.toSpecification(), jsonPathConfig);
|
||||
this.jsonPathDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig);
|
||||
this.errors = result.getErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(String path, Class<D> entityType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
return this.jsonPathDocument.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(String path, ParameterizedTypeReference<D> entityType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
return this.jsonPathDocument.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(String path, Class<D> elementType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
return this.jsonPathDocument.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(String path, ParameterizedTypeReference<D> elementType) {
|
||||
return this.documentContext.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
return this.jsonPathDocument.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
|
||||
}
|
||||
|
||||
private static JsonPath initJsonPath(String path) {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
private Configuration jsonPathConfig;
|
||||
|
||||
@Nullable
|
||||
private OperationContentLoader operationContentLoader;
|
||||
private DocumentSource documentSource;
|
||||
|
||||
|
||||
DefaultGraphQlClientBuilder(GraphQlTransport transport) {
|
||||
@@ -62,8 +62,8 @@ class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient.Builder operationContentLoader(@Nullable OperationContentLoader contentLoader) {
|
||||
this.operationContentLoader = contentLoader;
|
||||
public GraphQlClient.Builder documentSource(@Nullable DocumentSource contentLoader) {
|
||||
this.documentSource = contentLoader;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
}
|
||||
}
|
||||
|
||||
private OperationContentLoader initRequestNameResolver() {
|
||||
return (this.operationContentLoader == null ?
|
||||
new ResourceOperationContentLoader() : this.operationContentLoader);
|
||||
private DocumentSource initRequestNameResolver() {
|
||||
return (this.documentSource == null ?
|
||||
new ResourceDocumentSource() : this.documentSource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,19 +18,19 @@ package org.springframework.graphql.client;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Strategy to load the content of a GraphQL operation from a key.
|
||||
* Strategy to locate a GraphQL document identified by name.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface OperationContentLoader {
|
||||
public interface DocumentSource {
|
||||
|
||||
/**
|
||||
* Return the operation for the given key.
|
||||
* @param key the key to look up the operation content
|
||||
* @return the content of the operation, if found
|
||||
* Return the document that matches the given name.
|
||||
* @param name the name to use for the lookup
|
||||
* @return the document, or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
String loadOperation(String key);
|
||||
String getDocument(String name);
|
||||
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
public interface GraphQlClient {
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying transport or {@code null} if the required type
|
||||
* does not match the transport type. See {@link GraphQlTransport}
|
||||
@@ -47,21 +48,23 @@ public interface GraphQlClient {
|
||||
@Nullable
|
||||
<T extends GraphQlTransport> T getTransport(Class<T> requiredType);
|
||||
|
||||
/**
|
||||
* Start a new request with the given GraphQL operation, which can be a
|
||||
* query, a mutation, or subscription.
|
||||
* @param operation the operation to perform
|
||||
* @return spec to further define and execute the request
|
||||
*/
|
||||
RequestSpec operation(String operation);
|
||||
|
||||
/**
|
||||
* Variant of {@link #operation(String)} that loads the content of the
|
||||
* operation from the given key through the configured
|
||||
* {@link OperationContentLoader}.
|
||||
* Start defining a GraphQL request with the given document, which is the
|
||||
* textual representation of an operation (or operations) to perform,
|
||||
* including selection sets and fragments.
|
||||
* @param document the document for the request
|
||||
* @return spec to further define or execute the request
|
||||
*/
|
||||
RequestSpec document(String document);
|
||||
|
||||
/**
|
||||
* Variant of {@link #document(String)} that uses the given key to resolve
|
||||
* the GraphQL document from a file, or in another way with the help of the
|
||||
* {@link DocumentSource} that the client is configured with.
|
||||
* @throws IllegalArgumentException if the content could not be loaded
|
||||
*/
|
||||
RequestSpec loadOperationContent(String key);
|
||||
RequestSpec documentName(String name);
|
||||
|
||||
|
||||
/**
|
||||
@@ -87,11 +90,11 @@ public interface GraphQlClient {
|
||||
Builder jsonPathConfig(@Nullable Configuration config);
|
||||
|
||||
/**
|
||||
* Configure an {@link OperationContentLoader} to use with
|
||||
* {@link #loadOperationContent(String) GraphQlClient#loadOperationContent}.
|
||||
* <p>By default, {@link ResourceOperationContentLoader} is used.
|
||||
* Configure a {@link DocumentSource} for use with
|
||||
* {@link #documentName(String)} for resolving a document by name.
|
||||
* <p>By default, {@link ResourceDocumentSource} is used.
|
||||
*/
|
||||
Builder operationContentLoader(@Nullable OperationContentLoader contentLoader);
|
||||
Builder documentSource(@Nullable DocumentSource contentLoader);
|
||||
|
||||
/**
|
||||
* Build the {@code GraphQlClient} instance.
|
||||
@@ -102,12 +105,13 @@ public interface GraphQlClient {
|
||||
|
||||
|
||||
/**
|
||||
* Declare options to execute a request.
|
||||
* Declare options for GraphQL request execution.
|
||||
*/
|
||||
interface ExecuteSpec {
|
||||
|
||||
/**
|
||||
* Execute as a single-response operation such as "query" or "mutation".
|
||||
* Execute as a request with a single response such as a "query" or
|
||||
* "mutation" operation.
|
||||
* @return a {@code Mono} with a {@code ResponseSpec} for further
|
||||
* decoding of the response. The {@code Mono} may end wth an error due
|
||||
* to transport level issues.
|
||||
@@ -115,7 +119,7 @@ public interface GraphQlClient {
|
||||
Mono<ResponseSpec> execute();
|
||||
|
||||
/**
|
||||
* Execute a "subscription" request and stream a stream of responses.
|
||||
* Execute a "subscription" request with a stream of responses.
|
||||
* @return a {@code Flux} with a {@code ResponseSpec} for further
|
||||
* decoding of the response. The {@code Flux} may terminate as follows:
|
||||
* <ul>
|
||||
@@ -139,14 +143,15 @@ public interface GraphQlClient {
|
||||
interface RequestSpec extends ExecuteSpec {
|
||||
|
||||
/**
|
||||
* Set the operation name.
|
||||
* @param name the operation name
|
||||
* Set the name of the operation in the {@link #document(String) document}
|
||||
* to execute, if the document contains multiple operations.
|
||||
* @param operationName the operation name
|
||||
* @return this request spec
|
||||
*/
|
||||
RequestSpec operationName(@Nullable String name);
|
||||
RequestSpec operationName(@Nullable String operationName);
|
||||
|
||||
/**
|
||||
* Add a variable.
|
||||
* Add a value for a variable defined by the operation.
|
||||
* @param name the variable name
|
||||
* @param value the variable value
|
||||
* @return this request spec
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,7 @@ import graphql.ExecutionResult;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
|
||||
/**
|
||||
* Contract for a transport, over which to execute GraphQL requests.
|
||||
@@ -30,18 +30,19 @@ import org.springframework.graphql.RequestInput;
|
||||
public interface GraphQlTransport {
|
||||
|
||||
/**
|
||||
* Execute a single-response operation such as "query" or "mutation".
|
||||
* @param input the request to execute
|
||||
* Execute a request that returns a single response such as a "query" or a
|
||||
* "mutation" operation.
|
||||
* @param request the request to execute
|
||||
* @return a {@code Mono} with the {@code ExecutionResult} for the response.
|
||||
* The {@code Mono} may end wth an error due to transport or other issues
|
||||
* such as failures to encode the request or decode the response.
|
||||
* </ul>
|
||||
*/
|
||||
Mono<ExecutionResult> execute(RequestInput input);
|
||||
Mono<ExecutionResult> execute(GraphQlRequest request);
|
||||
|
||||
/**
|
||||
* Execute a "subscription" request and stream the responses.
|
||||
* @param input the request to execute
|
||||
* Execute a "subscription" request that returns a stream of responses.
|
||||
* @param request the request to execute
|
||||
* @return a {@code Flux} with an {@code ExecutionResult} for each response.
|
||||
* The {@code Flux} may terminate as follows:
|
||||
* <ul>
|
||||
@@ -54,6 +55,6 @@ public interface GraphQlTransport {
|
||||
* <p>The {@code Flux} may be cancelled to notify the server to end the
|
||||
* subscription stream.
|
||||
*/
|
||||
Flux<ExecutionResult> executeSubscription(RequestInput input);
|
||||
Flux<ExecutionResult> executeSubscription(GraphQlRequest request);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,17 +22,17 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Transport to execute GraphQL requests over HTTP via {@link WebClient}.
|
||||
* Single-response requests are performed over HTTP POST while subscriptions
|
||||
* over HTTP are not supported.
|
||||
* Supports only single-response requests over HTTP POST. For subscription
|
||||
* requests, see {@link WebSocketGraphQlTransport}.
|
||||
*
|
||||
* <p>Use the builder to initialize the transport and the {@link GraphQlClient}
|
||||
* <p>Use the builder to initialize the transport and the {@code GraphQlClient}
|
||||
* in a single chain:
|
||||
*
|
||||
* <pre style="class">
|
||||
@@ -73,18 +73,18 @@ public class HttpGraphQlTransport implements GraphQlTransport {
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ExecutionResult> execute(RequestInput requestInput) {
|
||||
public Mono<ExecutionResult> execute(GraphQlRequest request) {
|
||||
return this.webClient.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestInput.toMap())
|
||||
.bodyValue(request.toMap())
|
||||
.retrieve()
|
||||
.bodyToMono(MAP_TYPE)
|
||||
.map(MapExecutionResult::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput requestInput) {
|
||||
public Flux<ExecutionResult> executeSubscription(GraphQlRequest request) {
|
||||
throw new UnsupportedOperationException("Subscriptions not supported over HTTP");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* {@link OperationContentLoader} that looks for resources relative to a list of
|
||||
* {@link Resource} locations with a list of extensions.
|
||||
* {@link DocumentSource} that looks under a set of locations for a
|
||||
* {@link Resource} with the document name and a list of configured extensions.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ResourceOperationContentLoader implements OperationContentLoader {
|
||||
public class ResourceDocumentSource implements DocumentSource {
|
||||
|
||||
private static final List<String> FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql");
|
||||
|
||||
@@ -48,21 +48,21 @@ public class ResourceOperationContentLoader implements OperationContentLoader {
|
||||
* Default constructor to look under {@code graphql/} on the classpath for
|
||||
* resources with extensions ".graphql" and ".gql".
|
||||
*/
|
||||
public ResourceOperationContentLoader() {
|
||||
public ResourceDocumentSource() {
|
||||
this(Collections.singletonList(new ClassPathResource("graphql/")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with custom locations with extensions ".graphql" and ".gql".
|
||||
*/
|
||||
public ResourceOperationContentLoader(List<Resource> locations) {
|
||||
public ResourceDocumentSource(List<Resource> locations) {
|
||||
this(locations, FILE_EXTENSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with given locations and extensions.
|
||||
*/
|
||||
public ResourceOperationContentLoader(List<Resource> locations, List<String> extensions) {
|
||||
public ResourceDocumentSource(List<Resource> locations, List<String> extensions) {
|
||||
this.locations = new ArrayList<>(locations);
|
||||
this.extensions = new ArrayList<>(extensions);
|
||||
}
|
||||
@@ -84,9 +84,9 @@ public class ResourceOperationContentLoader implements OperationContentLoader {
|
||||
|
||||
|
||||
@Override
|
||||
public String loadOperation(String key) {
|
||||
public String getDocument(String name) {
|
||||
return this.locations.stream()
|
||||
.flatMap(location -> this.extensions.stream().map(ext -> getRelativeResource(location, key, ext)))
|
||||
.flatMap(location -> this.extensions.stream().map(ext -> getRelativeResource(location, name, ext)))
|
||||
.filter(Resource::exists)
|
||||
.findFirst()
|
||||
.map(resource -> {
|
||||
@@ -35,7 +35,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
@@ -156,13 +156,13 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ExecutionResult> execute(RequestInput input) {
|
||||
return this.graphQlSessionMono.flatMap(session -> session.execute(input));
|
||||
public Mono<ExecutionResult> execute(GraphQlRequest request) {
|
||||
return this.graphQlSessionMono.flatMap(session -> session.execute(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput input) {
|
||||
return this.graphQlSessionMono.flatMapMany(session -> session.executeSubscription(input));
|
||||
public Flux<ExecutionResult> executeSubscription(GraphQlRequest request) {
|
||||
return this.graphQlSessionMono.flatMapMany(session -> session.executeSubscription(request));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,10 +540,10 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport {
|
||||
return this.requestSink.asFlux();
|
||||
}
|
||||
|
||||
public Mono<ExecutionResult> execute(RequestInput requestInput) {
|
||||
public Mono<ExecutionResult> execute(GraphQlRequest request) {
|
||||
String id = String.valueOf(this.requestIndex.incrementAndGet());
|
||||
try {
|
||||
GraphQlWebSocketMessage message = GraphQlWebSocketMessage.subscribe(id, requestInput);
|
||||
GraphQlWebSocketMessage message = GraphQlWebSocketMessage.subscribe(id, request);
|
||||
Sinks.One<ExecutionResult> sink = Sinks.one();
|
||||
this.resultSinks.put(id, sink);
|
||||
trySend(message);
|
||||
@@ -555,10 +555,10 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport {
|
||||
}
|
||||
}
|
||||
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput requestInput) {
|
||||
public Flux<ExecutionResult> executeSubscription(GraphQlRequest request) {
|
||||
String id = String.valueOf(this.requestIndex.incrementAndGet());
|
||||
try {
|
||||
GraphQlWebSocketMessage message = GraphQlWebSocketMessage.subscribe(id, requestInput);
|
||||
GraphQlWebSocketMessage message = GraphQlWebSocketMessage.subscribe(id, request);
|
||||
Sinks.Many<ExecutionResult> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
this.streamingSinks.put(id, sink);
|
||||
trySend(message);
|
||||
|
||||
@@ -69,7 +69,7 @@ class HandlerMethodInputValidator {
|
||||
* Validate the {@link HandlerMethod} input before invocation, throwing
|
||||
* an {@link ConstraintViolationException} if validation fails.
|
||||
*
|
||||
* @param handlerMethod the handler method for the current query
|
||||
* @param handlerMethod the handler method for the current request
|
||||
* @param arguments the resolved arguments for the method invocation
|
||||
*/
|
||||
public void validate(HandlerMethod handlerMethod, Object[] arguments) {
|
||||
|
||||
@@ -101,8 +101,8 @@ public abstract class QueryByExampleDataFetcher<T> {
|
||||
|
||||
|
||||
/**
|
||||
* Prepare an {@link Example} from GraphQL query arguments.
|
||||
* @param env contextual info for the GraphQL query
|
||||
* Prepare an {@link Example} from GraphQL request arguments.
|
||||
* @param env contextual info for the GraphQL request
|
||||
* @return the resulting example
|
||||
*/
|
||||
@SuppressWarnings({"ConstantConditions", "unchecked"})
|
||||
|
||||
@@ -116,9 +116,9 @@ public abstract class QuerydslDataFetcher<T> {
|
||||
|
||||
|
||||
/**
|
||||
* Prepare a {@link Predicate} from GraphQL query arguments, also applying
|
||||
* Prepare a {@link Predicate} from GraphQL request arguments, also applying
|
||||
* any {@link QuerydslBinderCustomizer} that may have been configured.
|
||||
* @param environment contextual info for the GraphQL query
|
||||
* @param environment contextual info for the GraphQL request
|
||||
* @return the resulting predicate
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@@ -341,7 +341,7 @@ public abstract class QuerydslDataFetcher<T> {
|
||||
* {@link #autoRegistrationConfigurer(List, List) auto-registration}.
|
||||
* For manual registration, you will need to use this method to apply it.
|
||||
*
|
||||
* @param customizer to customize the GraphQL query to Querydsl
|
||||
* @param customizer to customize GraphQL request input to Querydsl
|
||||
* Predicate binding with
|
||||
* @return a new {@link Builder} instance with all previously configured
|
||||
* options and {@code QuerydslBinderCustomizer} applied
|
||||
|
||||
@@ -54,7 +54,7 @@ public abstract class ReactorContextManager {
|
||||
* Save the given Reactor {@link ContextView} in the an {@link ExecutionInput} for
|
||||
* later access through the {@link DataFetchingEnvironment}.
|
||||
* @param contextView the reactor context view
|
||||
* @param input the GraphQL query input
|
||||
* @param input the input prepared from the GraphQL request
|
||||
*/
|
||||
static void setReactorContext(ContextView contextView, ExecutionInput input) {
|
||||
input.getGraphQLContext().put(CONTEXT_VIEW_KEY, contextView);
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Top level abstractions for processing GraphQL requests including
|
||||
* {@link org.springframework.graphql.GraphQlService} for executing a request and
|
||||
* {@link org.springframework.graphql.RequestInput} to represent the input for a request.
|
||||
* Top level abstractions for processing GraphQL requests.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -48,16 +48,13 @@ public class WebInput extends RequestInput {
|
||||
* Create an instance.
|
||||
* @param uri the URL for the HTTP request or WebSocket handshake
|
||||
* @param headers the HTTP request headers
|
||||
* @param body the content of the request deserialized from JSON
|
||||
* @param body the deserialized content of the GraphQL request
|
||||
* @param locale the locale from the HTTP request, if any
|
||||
* @param id an identifier for the GraphQL request, e.g. a subscription id for
|
||||
* correlating request and response messages, or it could be an id associated with the
|
||||
* underlying request/connection id, if available
|
||||
*/
|
||||
public WebInput(
|
||||
URI uri, HttpHeaders headers, Map<String, Object> body,
|
||||
@Nullable Locale locale, String id) {
|
||||
|
||||
public WebInput(URI uri, HttpHeaders headers, Map<String, Object> body, @Nullable Locale locale, String id) {
|
||||
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), locale, id);
|
||||
Assert.notNull(uri, "URI is required'");
|
||||
Assert.notNull(headers, "HttpHeaders is required'");
|
||||
@@ -68,7 +65,7 @@ public class WebInput extends RequestInput {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getKey(String key, Map<String, Object> body) {
|
||||
if (key.equals("query") && !StringUtils.hasText((String) body.get(key))) {
|
||||
throw new ServerWebInputException("Query is required");
|
||||
throw new ServerWebInputException("No \"query\" in the request input");
|
||||
}
|
||||
return (T) body.get(key);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,15 +24,15 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Interceptor for intercepting GraphQL over HTTP or GraphQL over WebSocket
|
||||
* requests. Provides information about the HTTP request or WebSocket handshake,
|
||||
* and allows customization of the {@link ExecutionInput} as well as of the
|
||||
* {@link ExecutionResult}.
|
||||
* Interceptor for the handling of GraphQL over HTTP or GraphQL over WebSocket
|
||||
* requests. Exposes the details of the underlying HTTP request or WebSocket
|
||||
* handshake, the decoded GraphQL request, and allows customization of the
|
||||
* {@link ExecutionInput} and the resulting {@link ExecutionResult}.
|
||||
*
|
||||
* <p> Interceptors are typically declared as beans in Spring configuration and
|
||||
* <p>Interceptors are typically declared as beans in Spring configuration and
|
||||
* ordered as defined in {@link ObjectProvider#orderedStream()}.
|
||||
*
|
||||
* <p> Supported for Spring MVC and WebFlux.
|
||||
* <p>Supported for Spring MVC and WebFlux.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
@@ -41,26 +41,27 @@ import org.springframework.util.Assert;
|
||||
public interface WebInterceptor {
|
||||
|
||||
/**
|
||||
* Intercept a request and possibly delegate to the rest of the chain
|
||||
* consisting of more interceptors as well as a
|
||||
* {@link org.springframework.graphql.GraphQlService} at the end to actually
|
||||
* handle the request through the GraphQL engine.
|
||||
* @param webInput container for HTTP request information and options to
|
||||
* customize the {@link ExecutionInput}.
|
||||
* @param chain the rest of the chain to delegate to for request execution
|
||||
* Intercept a request and delegate to the rest of the chain that consists
|
||||
* of other interceptors followed by a
|
||||
* {@link org.springframework.graphql.GraphQlService} that executes the
|
||||
* request through the GraphQL Java.
|
||||
* @param webInput provides access to GraphQL request input and allows
|
||||
* customizing the {@link ExecutionInput} that will be used.
|
||||
* @param chain the rest of the chain to handle the request
|
||||
* @return a {@link Mono} with the result
|
||||
*/
|
||||
Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain);
|
||||
|
||||
/**
|
||||
* Return a composed {@link WebInterceptor} that invokes the current
|
||||
* interceptor first and then the one that is passed in.
|
||||
* @param interceptor the interceptor to delegate to after "this" interceptor
|
||||
* @return the composed WebInterceptor
|
||||
* Return a new {@link WebInterceptor} that invokes the current interceptor
|
||||
* first and then the one that is passed in.
|
||||
* @param interceptor the interceptor to delegate to after "this"
|
||||
* @return the new {@code WebInterceptor}
|
||||
*/
|
||||
default WebInterceptor andThen(WebInterceptor interceptor) {
|
||||
Assert.notNull(interceptor, "WebInterceptor must not be null");
|
||||
return (currentInput, next) -> intercept(currentInput, (nextInput) -> interceptor.intercept(nextInput, next));
|
||||
Assert.notNull(interceptor, "WebInterceptor is required");
|
||||
return (currentInput, next) -> intercept(currentInput,
|
||||
(nextInput) -> interceptor.intercept(nextInput, next));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
package org.springframework.graphql.web;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Contract that allows a {@link WebInterceptor} to delegate to the remainder
|
||||
* of the chain.
|
||||
* Allows a {@link WebInterceptor} to invoke the rest of the chain.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
@@ -27,10 +27,11 @@ import reactor.core.publisher.Mono;
|
||||
public interface WebInterceptorChain {
|
||||
|
||||
/**
|
||||
* Delegate to the next rest of the chain consisting of more interceptors
|
||||
* as well as a {@link org.springframework.graphql.GraphQlService} at the
|
||||
* end to actually handle the request through the GraphQL engine.
|
||||
* @param webInput the input for the request
|
||||
* Delegate to the rest of the chain that consists of other interceptors
|
||||
* followed by a {@link org.springframework.graphql.GraphQlService} that
|
||||
* executes the request through the GraphQL Java.
|
||||
* @param webInput provides access to GraphQL request input and allows
|
||||
* customizing the {@link ExecutionInput} that will be used.
|
||||
* @return the output with the result from request execution
|
||||
*/
|
||||
Mono<WebOutput> next(WebInput webInput);
|
||||
|
||||
@@ -78,9 +78,7 @@ public class GraphQlHttpHandler {
|
||||
logger.debug("Execution complete");
|
||||
}
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
if (output.getResponseHeaders() != null) {
|
||||
builder.headers((headers) -> headers.putAll(output.getResponseHeaders()));
|
||||
}
|
||||
builder.headers(headers -> headers.putAll(output.getResponseHeaders()));
|
||||
return builder.bodyValue(spec);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Single response operation (query or mutation)
|
||||
// Single response (query or mutation)
|
||||
outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output) :
|
||||
Flux.error(new IllegalStateException("Execution failed: " + output.getErrors())));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Collections;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@@ -137,8 +137,8 @@ public class GraphQlWebSocketMessage {
|
||||
/**
|
||||
* Create a "subscribe" message.
|
||||
*/
|
||||
public static GraphQlWebSocketMessage subscribe(String id, RequestInput input) {
|
||||
return new GraphQlWebSocketMessage(id, "subscribe", input.toMap());
|
||||
public static GraphQlWebSocketMessage subscribe(String id, GraphQlRequest request) {
|
||||
return new GraphQlWebSocketMessage(id, "subscribe", request.toMap());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,9 +87,7 @@ public class GraphQlHttpHandler {
|
||||
logger.debug("Execution complete");
|
||||
}
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
if (output.getResponseHeaders() != null) {
|
||||
builder.headers((headers) -> headers.putAll(output.getResponseHeaders()));
|
||||
}
|
||||
builder.headers(headers -> headers.putAll(output.getResponseHeaders()));
|
||||
return builder.body(output.toSpecification());
|
||||
});
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Single response operation (query or mutation)
|
||||
// Single response (query or mutation)
|
||||
outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output)
|
||||
: Flux.error(new IllegalStateException("Execution failed: " + output.getErrors())));
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -58,7 +58,7 @@ public class DefaultGraphQlClientTests {
|
||||
TestTransport transport = new TestTransport(result);
|
||||
|
||||
Project project = GraphQlClient.builder(transport).build()
|
||||
.operation(query)
|
||||
.document(query)
|
||||
.execute()
|
||||
.map(spec -> spec.toEntity("project", Project.class))
|
||||
.block();
|
||||
@@ -75,7 +75,7 @@ public class DefaultGraphQlClientTests {
|
||||
private final Mono<ExecutionResult> response;
|
||||
|
||||
@Nullable
|
||||
private RequestInput savedRequestInput;
|
||||
private GraphQlRequest savedRequest;
|
||||
|
||||
public TestTransport(ExecutionResult response) {
|
||||
this(Mono.just(response));
|
||||
@@ -85,19 +85,19 @@ public class DefaultGraphQlClientTests {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public RequestInput getSavedRequestInput() {
|
||||
Assert.notNull(this.savedRequestInput, "No saved RequestInput");
|
||||
return this.savedRequestInput;
|
||||
public GraphQlRequest getSavedRequest() {
|
||||
Assert.notNull(this.savedRequest, "No saved request");
|
||||
return this.savedRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ExecutionResult> execute(RequestInput input) {
|
||||
this.savedRequestInput = input;
|
||||
public Mono<ExecutionResult> execute(GraphQlRequest request) {
|
||||
this.savedRequest = request;
|
||||
return this.response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ExecutionResult> executeSubscription(RequestInput input) {
|
||||
public Flux<ExecutionResult> executeSubscription(GraphQlRequest request) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -71,36 +72,36 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
|
||||
@Test
|
||||
void request() {
|
||||
RequestInput input = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
|
||||
StepVerifier.create(this.transport.execute(input))
|
||||
StepVerifier.create(this.transport.execute(request))
|
||||
.expectNext(this.result1).expectComplete()
|
||||
.verify(TIMEOUT);
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input));
|
||||
GraphQlWebSocketMessage.subscribe("1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestStream() {
|
||||
RequestInput input = this.mockServer.expectOperation("Sub1").andStream(Flux.just(this.result1, result2));
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Sub1").andStream(Flux.just(this.result1, result2));
|
||||
|
||||
StepVerifier.create(this.transport.executeSubscription(input))
|
||||
StepVerifier.create(this.transport.executeSubscription(request))
|
||||
.expectNext(this.result1, result2).expectComplete()
|
||||
.verify(TIMEOUT);
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input));
|
||||
GraphQlWebSocketMessage.subscribe("1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestError() {
|
||||
RequestInput input = this.mockServer.expectOperation("Query1")
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Query1")
|
||||
.andRespondWithError(GraphqlErrorBuilder.newError().message("boo").build());
|
||||
|
||||
StepVerifier.create(this.transport.execute(input))
|
||||
StepVerifier.create(this.transport.execute(request))
|
||||
.consumeNextWith(result -> {
|
||||
assertThat(result.isDataPresent()).isFalse();
|
||||
assertThat(result.getErrors()).extracting(GraphQLError::getMessage).containsExactly("boo");
|
||||
@@ -110,15 +111,15 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input));
|
||||
GraphQlWebSocketMessage.subscribe("1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestStreamError() {
|
||||
RequestInput input = this.mockServer.expectOperation("Sub1")
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Sub1")
|
||||
.andStreamWithError(Flux.just(this.result1), GraphqlErrorBuilder.newError().message("boo").build());
|
||||
|
||||
StepVerifier.create(this.transport.executeSubscription(input))
|
||||
StepVerifier.create(this.transport.executeSubscription(request))
|
||||
.expectNext(this.result1)
|
||||
.expectErrorSatisfies(actualEx -> {
|
||||
List<GraphQLError> errorList = ((SubscriptionErrorException) actualEx).getErrors();
|
||||
@@ -128,29 +129,29 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input));
|
||||
GraphQlWebSocketMessage.subscribe("1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestCancelled() {
|
||||
RequestInput input = this.mockServer.expectOperation("Query1").andRespond(Mono.never());
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Query1").andRespond(Mono.never());
|
||||
|
||||
StepVerifier.create(this.transport.execute(input))
|
||||
StepVerifier.create(this.transport.execute(request))
|
||||
.thenAwait(Duration.ofMillis(200))
|
||||
.thenCancel()
|
||||
.verify(TIMEOUT);
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input));
|
||||
GraphQlWebSocketMessage.subscribe("1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestStreamCancelled() {
|
||||
RequestInput input = this.mockServer.expectOperation("s1")
|
||||
GraphQlRequest request = this.mockServer.expectOperation("s1")
|
||||
.andStream(Flux.just(this.result1).concatWith(Flux.never()));
|
||||
|
||||
StepVerifier.create(this.transport.executeSubscription(input))
|
||||
StepVerifier.create(this.transport.executeSubscription(request))
|
||||
.expectNext(this.result1)
|
||||
.thenAwait(Duration.ofMillis(200))
|
||||
.thenCancel()
|
||||
@@ -158,7 +159,7 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
|
||||
assertActualClientMessages(
|
||||
GraphQlWebSocketMessage.connectionInit(null),
|
||||
GraphQlWebSocketMessage.subscribe("1", input),
|
||||
GraphQlWebSocketMessage.subscribe("1", request),
|
||||
GraphQlWebSocketMessage.complete("1"));
|
||||
}
|
||||
|
||||
@@ -197,8 +198,8 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
assertThat(this.testClient.getConnection(0).closeStatus().block(TIMEOUT)).isEqualTo(CloseStatus.NORMAL);
|
||||
|
||||
// New requests are rejected
|
||||
RequestInput input = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(input))
|
||||
GraphQlRequest request = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(request))
|
||||
.expectErrorMessage("WebSocketGraphQlTransport has been stopped")
|
||||
.verify(TIMEOUT);
|
||||
|
||||
@@ -208,8 +209,8 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
assertThat(this.testClient.getConnection(1).isOpen()).isTrue();
|
||||
|
||||
// Requests allowed again
|
||||
input = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(input))
|
||||
request = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(request))
|
||||
.expectNext(this.result1).expectComplete()
|
||||
.verify(TIMEOUT);
|
||||
}
|
||||
@@ -217,14 +218,14 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
@Test
|
||||
void sessionIsCachedUntilClosed() {
|
||||
|
||||
RequestInput input1 = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(input1)).expectNext(this.result1).expectComplete().verify(TIMEOUT);
|
||||
GraphQlRequest request1 = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(request1)).expectNext(this.result1).expectComplete().verify(TIMEOUT);
|
||||
|
||||
assertThat(this.testClient.getConnectionCount()).isEqualTo(1);
|
||||
TestWebSocketConnection originalConnection = this.testClient.getConnection(0);
|
||||
|
||||
RequestInput input2 = this.mockServer.expectOperation("Query2").andRespond(this.result2);
|
||||
StepVerifier.create(this.transport.execute(input2)).expectNext(this.result2).expectComplete().verify(TIMEOUT);
|
||||
GraphQlRequest request2 = this.mockServer.expectOperation("Query2").andRespond(this.result2);
|
||||
StepVerifier.create(this.transport.execute(request2)).expectNext(this.result2).expectComplete().verify(TIMEOUT);
|
||||
|
||||
assertThat(this.testClient.getConnectionCount()).isEqualTo(1);
|
||||
assertThat(this.testClient.getConnection(0)).isSameAs(originalConnection);
|
||||
@@ -232,8 +233,8 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
// Close the connection
|
||||
originalConnection.closeServerSession(CloseStatus.NORMAL).block(TIMEOUT);
|
||||
|
||||
input1 = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(input1)).expectNext(this.result1).expectComplete().verify(TIMEOUT);
|
||||
request1 = this.mockServer.expectOperation("Query1").andRespond(this.result1);
|
||||
StepVerifier.create(this.transport.execute(request1)).expectNext(this.result1).expectComplete().verify(TIMEOUT);
|
||||
|
||||
assertThat(this.testClient.getConnectionCount()).isEqualTo(2);
|
||||
assertThat(this.testClient.getConnection(1)).isNotSameAs(originalConnection);
|
||||
@@ -280,7 +281,7 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
String expectedMessage = "GraphQlSession over client-session-1 disconnected " +
|
||||
"with CloseStatus[code=1002, reason=null]";
|
||||
|
||||
StepVerifier.create(transport.execute(new RequestInput("Query1", null, null, null, "")))
|
||||
StepVerifier.create(transport.execute(new GraphQlRequest("Query1")))
|
||||
.expectErrorMessage(expectedMessage)
|
||||
.verify(TIMEOUT);
|
||||
}
|
||||
@@ -321,7 +322,7 @@ public class MockWebSocketGraphQlTransportTests {
|
||||
|
||||
GraphQlWebSocketMessage responseMessage = (requestMessage.getType().equals("connection_init") ?
|
||||
GraphQlWebSocketMessage.connectionAck(null) :
|
||||
GraphQlWebSocketMessage.subscribe(id, new RequestInput("..", null, null, null, "")));
|
||||
GraphQlWebSocketMessage.subscribe(id, new GraphQlRequest("")));
|
||||
|
||||
return Flux.just(this.codecDelegate.encode(session, responseMessage));
|
||||
}));
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.RequestInput;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
@@ -63,7 +63,7 @@ public class MockWebSocketServer implements WebSocketHandler {
|
||||
*/
|
||||
public ResponseSpec expectOperation(String operation) {
|
||||
Exchange exchange = new Exchange(operation);
|
||||
this.expectedExchanges.put(exchange.getInput().toMap(), exchange);
|
||||
this.expectedExchanges.put(exchange.getRequest().toMap(), exchange);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
@@ -112,34 +112,34 @@ public class MockWebSocketServer implements WebSocketHandler {
|
||||
/**
|
||||
* Respond with the given a single result.
|
||||
*/
|
||||
RequestInput andRespond(ExecutionResult result);
|
||||
GraphQlRequest andRespond(ExecutionResult result);
|
||||
|
||||
/**
|
||||
* Respond with the given a single result {@code Mono}.
|
||||
*/
|
||||
RequestInput andRespond(Mono<ExecutionResult> resultMono);
|
||||
GraphQlRequest andRespond(Mono<ExecutionResult> resultMono);
|
||||
|
||||
/**
|
||||
* Respond with a GraphQL over WebSocket "error" message.
|
||||
*/
|
||||
RequestInput andRespondWithError(GraphQLError error);
|
||||
GraphQlRequest andRespondWithError(GraphQLError error);
|
||||
|
||||
/**
|
||||
* Respond with the given stream of responses.
|
||||
*/
|
||||
RequestInput andStream(Flux<ExecutionResult> resultFlux);
|
||||
GraphQlRequest andStream(Flux<ExecutionResult> resultFlux);
|
||||
|
||||
/**
|
||||
* Respond with the given stream of responses and terminate with an error.
|
||||
*/
|
||||
RequestInput andStreamWithError(Flux<ExecutionResult> resultFlux, GraphQLError error);
|
||||
GraphQlRequest andStreamWithError(Flux<ExecutionResult> resultFlux, GraphQLError error);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class Exchange implements ResponseSpec {
|
||||
|
||||
private final RequestInput requestInput;
|
||||
private final GraphQlRequest request;
|
||||
|
||||
private Flux<ExecutionResult> responseFlux = Flux.empty();
|
||||
|
||||
@@ -148,42 +148,42 @@ public class MockWebSocketServer implements WebSocketHandler {
|
||||
|
||||
|
||||
private Exchange(String operation) {
|
||||
this.requestInput = new RequestInput(operation, null, null, null, "");
|
||||
this.request = new GraphQlRequest(operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestInput andRespond(ExecutionResult result) {
|
||||
public GraphQlRequest andRespond(ExecutionResult result) {
|
||||
return addResponse(Flux.just(result), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestInput andRespond(Mono<ExecutionResult> resultMono) {
|
||||
public GraphQlRequest andRespond(Mono<ExecutionResult> resultMono) {
|
||||
return addResponse(Flux.from(resultMono), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestInput andRespondWithError(GraphQLError error) {
|
||||
public GraphQlRequest andRespondWithError(GraphQLError error) {
|
||||
return addResponse(Flux.empty(), error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestInput andStream(Flux<ExecutionResult> resultFlux) {
|
||||
public GraphQlRequest andStream(Flux<ExecutionResult> resultFlux) {
|
||||
return addResponse(resultFlux, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestInput andStreamWithError(Flux<ExecutionResult> resultFlux, GraphQLError error) {
|
||||
public GraphQlRequest andStreamWithError(Flux<ExecutionResult> resultFlux, GraphQLError error) {
|
||||
return addResponse(resultFlux, error);
|
||||
}
|
||||
|
||||
private RequestInput addResponse(Flux<ExecutionResult> resultFlux, @Nullable GraphQLError error) {
|
||||
private GraphQlRequest addResponse(Flux<ExecutionResult> resultFlux, @Nullable GraphQLError error) {
|
||||
this.responseFlux = resultFlux;
|
||||
this.error = error;
|
||||
return this.requestInput;
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RequestInput getInput() {
|
||||
return this.requestInput;
|
||||
public GraphQlRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public Flux<ExecutionResult> getResponseFlux() {
|
||||
|
||||
Reference in New Issue
Block a user