[JAVA-14660] Update code for Graphql error handling (#12839)
This commit is contained in:
parent
d6918ce11d
commit
4897b0afa0
@ -1,3 +0,0 @@
|
|||||||
### Relevant Articles:
|
|
||||||
|
|
||||||
- [Error Handling in GraphQL With Spring Boot](https://www.baeldung.com/spring-graphql-error-handling)
|
|
@ -1,80 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<artifactId>graphql-error-handling</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
<name>graphql-error-handling</name>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>com.baeldung.graphql</groupId>
|
|
||||||
<artifactId>graphql-modules</artifactId>
|
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<!-- Boot version compatible with graphql-spring-boot-starter -->
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
|
||||||
<version>2.6.4</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.graphql-java</groupId>
|
|
||||||
<artifactId>graphql-spring-boot-starter</artifactId>
|
|
||||||
<version>${graphql-spring-boot-starter.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.graphql-java</groupId>
|
|
||||||
<artifactId>graphql-java-tools</artifactId>
|
|
||||||
<version>${graphql-java-tools.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.h2database</groupId>
|
|
||||||
<artifactId>h2</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.graphql-java</groupId>
|
|
||||||
<artifactId>graphql-spring-boot-starter-test</artifactId>
|
|
||||||
<version>${graphql-spring-boot-starter.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.skyscreamer</groupId>
|
|
||||||
<artifactId>jsonassert</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<graphql-spring-boot-starter.version>5.0.2</graphql-spring-boot-starter.version>
|
|
||||||
<graphql-java-tools.version>5.2.4</graphql-java-tools.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
</project>
|
|
@ -1,46 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling;
|
|
||||||
|
|
||||||
import com.baeldung.graphql.error.handling.exception.GraphQLErrorAdapter;
|
|
||||||
import graphql.ExceptionWhileDataFetching;
|
|
||||||
import graphql.GraphQLError;
|
|
||||||
import graphql.servlet.GraphQLErrorHandler;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
|
||||||
public class GraphQLErrorHandlerApplication {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(GraphQLErrorHandlerApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public GraphQLErrorHandler errorHandler() {
|
|
||||||
return new GraphQLErrorHandler() {
|
|
||||||
@Override
|
|
||||||
public List<GraphQLError> processErrors(List<GraphQLError> errors) {
|
|
||||||
List<GraphQLError> clientErrors = errors.stream()
|
|
||||||
.filter(this::isClientError)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<GraphQLError> serverErrors = errors.stream()
|
|
||||||
.filter(e -> !isClientError(e))
|
|
||||||
.map(GraphQLErrorAdapter::new)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<GraphQLError> e = new ArrayList<>();
|
|
||||||
e.addAll(clientErrors);
|
|
||||||
e.addAll(serverErrors);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isClientError(GraphQLError error) {
|
|
||||||
return !(error instanceof ExceptionWhileDataFetching || error instanceof Throwable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling.exception;
|
|
||||||
|
|
||||||
import graphql.ErrorType;
|
|
||||||
import graphql.GraphQLError;
|
|
||||||
import graphql.language.SourceLocation;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class AbstractGraphQLException extends RuntimeException implements GraphQLError {
|
|
||||||
private Map<String, Object> parameters = new HashMap();
|
|
||||||
|
|
||||||
public AbstractGraphQLException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractGraphQLException(String message, Map<String, Object> additionParams) {
|
|
||||||
this(message);
|
|
||||||
if (additionParams != null) {
|
|
||||||
parameters = additionParams;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMessage() {
|
|
||||||
return super.getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<SourceLocation> getLocations() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ErrorType getErrorType() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getExtensions() {
|
|
||||||
return this.parameters;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling.exception;
|
|
||||||
|
|
||||||
import graphql.ErrorType;
|
|
||||||
import graphql.ExceptionWhileDataFetching;
|
|
||||||
import graphql.GraphQLError;
|
|
||||||
import graphql.language.SourceLocation;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class GraphQLErrorAdapter implements GraphQLError {
|
|
||||||
|
|
||||||
private GraphQLError error;
|
|
||||||
|
|
||||||
public GraphQLErrorAdapter(GraphQLError error) {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getExtensions() {
|
|
||||||
return error.getExtensions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<SourceLocation> getLocations() {
|
|
||||||
return error.getLocations();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ErrorType getErrorType() {
|
|
||||||
return error.getErrorType();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Object> getPath() {
|
|
||||||
return error.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> toSpecification() {
|
|
||||||
return error.toSpecification();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMessage() {
|
|
||||||
return (error instanceof ExceptionWhileDataFetching) ? ((ExceptionWhileDataFetching) error).getException().getMessage() : error.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling.exception;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class VehicleNotFoundException extends AbstractGraphQLException {
|
|
||||||
|
|
||||||
public VehicleNotFoundException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VehicleNotFoundException(String message, Map<String, Object> params) {
|
|
||||||
super(message, params);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling.resolver;
|
|
||||||
|
|
||||||
import com.baeldung.graphql.error.handling.domain.Location;
|
|
||||||
import com.baeldung.graphql.error.handling.domain.Vehicle;
|
|
||||||
import com.baeldung.graphql.error.handling.service.InventoryService;
|
|
||||||
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class Mutation implements GraphQLMutationResolver {
|
|
||||||
private InventoryService inventoryService;
|
|
||||||
|
|
||||||
public Mutation(InventoryService inventoryService) {
|
|
||||||
this.inventoryService = inventoryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vehicle addVehicle(String vin, Integer year, String make, String model, String trim, Location location) {
|
|
||||||
return this.inventoryService.addVehicle(vin, year, make, model, trim, location);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling.resolver;
|
|
||||||
|
|
||||||
import com.baeldung.graphql.error.handling.domain.Vehicle;
|
|
||||||
import com.baeldung.graphql.error.handling.service.InventoryService;
|
|
||||||
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class Query implements GraphQLQueryResolver {
|
|
||||||
private final InventoryService inventoryService;
|
|
||||||
|
|
||||||
public Query(InventoryService inventoryService) {
|
|
||||||
this.inventoryService = inventoryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Vehicle> searchAll() {
|
|
||||||
return this.inventoryService.searchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Vehicle> searchByLocation(String zipcode) {
|
|
||||||
return this.inventoryService.searchByLocation(zipcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vehicle searchByVin(String vin) {
|
|
||||||
return this.inventoryService.searchByVin(vin);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
insert into LOCATION values('07092', 'Mountainside', 'NJ');
|
|
||||||
insert into LOCATION values ('94118', 'San Francisco', 'CA');
|
|
||||||
insert into LOCATION values ('10002', 'New York', 'NY');
|
|
||||||
|
|
||||||
insert into VEHICLE (vin, year, make, model, trim, fk_location) values('KM8JN72DX7U587496', 2007, 'Hyundai', 'Tucson', null, '07092');
|
|
||||||
insert into VEHICLE (vin, year, make, model, trim, fk_location) values('JTKKU4B41C1023346', 2012, 'Toyota', 'Scion', 'Xd', '94118');
|
|
||||||
insert into VEHICLE (vin, year, make, model, trim, fk_location) values('1G1JC1444PZ215071', 2000, 'Chevrolet', 'CAVALIER VL', 'RS', '07092');
|
|
@ -1,55 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling;
|
|
||||||
|
|
||||||
import com.graphql.spring.boot.test.GraphQLResponse;
|
|
||||||
import com.graphql.spring.boot.test.GraphQLTestTemplate;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
|
||||||
import org.springframework.test.context.junit4.SpringRunner;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import static com.baeldung.graphql.error.handling.TestUtils.readFile;
|
|
||||||
import static java.lang.String.format;
|
|
||||||
|
|
||||||
@RunWith(SpringRunner.class)
|
|
||||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = GraphQLErrorHandlerApplication.class)
|
|
||||||
public class GraphQLErrorHandlerApplicationIntegrationTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private GraphQLTestTemplate graphQLTestTemplate;
|
|
||||||
|
|
||||||
private static final String GRAPHQL_TEST_REQUEST_PATH = "graphql/request/%s.graphql";
|
|
||||||
private static final String GRAPHQL_TEST_RESPONSE_PATH = "graphql/response/%s.json";
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenUnknownOperation_thenRespondWithRequestError() throws IOException, JSONException {
|
|
||||||
String graphqlName = "request_error_unknown_operation";
|
|
||||||
GraphQLResponse actualResponse = graphQLTestTemplate.postForResource(format(GRAPHQL_TEST_REQUEST_PATH, graphqlName));
|
|
||||||
String expectedResponse = readFile(format(GRAPHQL_TEST_RESPONSE_PATH, graphqlName));
|
|
||||||
|
|
||||||
JSONAssert.assertEquals(expectedResponse, actualResponse.getRawResponse().getBody(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenInvalidSyntaxRequest_thenRespondWithRequestError() throws IOException, JSONException {
|
|
||||||
String graphqlName = "request_error_invalid_request_syntax";
|
|
||||||
GraphQLResponse actualResponse = graphQLTestTemplate.postForResource(format(GRAPHQL_TEST_REQUEST_PATH, graphqlName));
|
|
||||||
String expectedResponse = readFile(format(GRAPHQL_TEST_RESPONSE_PATH, graphqlName));
|
|
||||||
|
|
||||||
JSONAssert.assertEquals(expectedResponse, actualResponse.getRawResponse().getBody(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenRequestAllNonNullField_thenRespondPartialDataWithFieldError() throws IOException, JSONException {
|
|
||||||
String graphqlName = "field_error_request_non_null_fields_partial_response";
|
|
||||||
GraphQLResponse actualResponse = graphQLTestTemplate.postForResource(format(GRAPHQL_TEST_REQUEST_PATH, graphqlName));
|
|
||||||
String expectedResponse = readFile(format(GRAPHQL_TEST_RESPONSE_PATH, graphqlName));
|
|
||||||
|
|
||||||
JSONAssert.assertEquals(expectedResponse, actualResponse.getRawResponse().getBody(), true);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package com.baeldung.graphql.error.handling;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
public class TestUtils {
|
|
||||||
public static String readFile(String path) throws IOException {
|
|
||||||
return IOUtils.toString(
|
|
||||||
new ClassPathResource(path).getInputStream(), Charset.defaultCharset()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
# trim is non null but one of the record has null value for trim
|
|
||||||
query {
|
|
||||||
searchAll {
|
|
||||||
vin
|
|
||||||
year
|
|
||||||
make
|
|
||||||
model
|
|
||||||
trim
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"data": {
|
|
||||||
"searchAll": [
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
"vin": "JTKKU4B41C1023346",
|
|
||||||
"year": 2012,
|
|
||||||
"make": "Toyota",
|
|
||||||
"model": "Scion",
|
|
||||||
"trim": "Xd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vin": "1G1JC1444PZ215071",
|
|
||||||
"year": 2000,
|
|
||||||
"make": "Chevrolet",
|
|
||||||
"model": "CAVALIER VL",
|
|
||||||
"trim": "RS"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
|
|
||||||
"path": [
|
|
||||||
"searchAll",
|
|
||||||
0,
|
|
||||||
"trim"
|
|
||||||
],
|
|
||||||
"errorType": "DataFetchingException",
|
|
||||||
"locations": null,
|
|
||||||
"extensions": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"data": null,
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"message": "Invalid Syntax",
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"line": 5,
|
|
||||||
"column": 8,
|
|
||||||
"sourceName": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"errorType": "InvalidSyntax",
|
|
||||||
"path": null,
|
|
||||||
"extensions": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"data": null,
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"errorType": "OperationNotSupported",
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"line": 1,
|
|
||||||
"column": 1,
|
|
||||||
"sourceName": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extensions": null,
|
|
||||||
"message": "Schema is not configured for subscriptions.",
|
|
||||||
"path": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -18,9 +18,8 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>graphql-dgs</module>
|
<module>graphql-dgs</module>
|
||||||
<module>graphql-error-handling</module>
|
|
||||||
<module>graphql-java</module>
|
<module>graphql-java</module>
|
||||||
<module>graphql-spqr</module>
|
<module>graphql-spqr</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -9,6 +9,7 @@ The "REST With Spring" Classes: http://bit.ly/restwithspring
|
|||||||
|
|
||||||
- [Getting Started with GraphQL and Spring Boot](https://www.baeldung.com/spring-graphql)
|
- [Getting Started with GraphQL and Spring Boot](https://www.baeldung.com/spring-graphql)
|
||||||
- [Expose GraphQL Field with Different Name](https://www.baeldung.com/graphql-field-name)
|
- [Expose GraphQL Field with Different Name](https://www.baeldung.com/graphql-field-name)
|
||||||
|
- [Error Handling in GraphQL With Spring Boot](https://www.baeldung.com/spring-graphql-error-handling)
|
||||||
|
|
||||||
### GraphQL sample queries
|
### GraphQL sample queries
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-graphql</artifactId>
|
<artifactId>spring-boot-starter-graphql</artifactId>
|
||||||
@ -26,11 +30,21 @@
|
|||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.graphql</groupId>
|
<groupId>org.springframework.graphql</groupId>
|
||||||
<artifactId>spring-graphql-test</artifactId>
|
<artifactId>spring-graphql-test</artifactId>
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.baeldung.graphql.error.handling;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class GraphQLErrorHandlerApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.setProperty("spring.profiles.default", "error-handling");
|
||||||
|
SpringApplication.run(GraphQLErrorHandlerApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package com.baeldung.graphql.error.handling.controller;
|
||||||
|
|
||||||
|
import com.baeldung.graphql.error.handling.domain.Location;
|
||||||
|
import com.baeldung.graphql.error.handling.domain.Vehicle;
|
||||||
|
import com.baeldung.graphql.error.handling.service.InventoryService;
|
||||||
|
import org.springframework.graphql.data.method.annotation.Argument;
|
||||||
|
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||||
|
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class VehicleController {
|
||||||
|
|
||||||
|
private final InventoryService inventoryService;
|
||||||
|
|
||||||
|
public VehicleController(InventoryService inventoryService) {
|
||||||
|
this.inventoryService = inventoryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@QueryMapping
|
||||||
|
public List<Vehicle> searchAll() {
|
||||||
|
return this.inventoryService.searchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@QueryMapping
|
||||||
|
public List<Vehicle> searchByLocation(@Argument String zipcode) {
|
||||||
|
return this.inventoryService.searchByLocation(zipcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@QueryMapping
|
||||||
|
public Vehicle searchByVin(@Argument String vin) {
|
||||||
|
return this.inventoryService.searchByVin(vin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@MutationMapping
|
||||||
|
public Vehicle addVehicle(@Argument String vin, @Argument Integer year,
|
||||||
|
@Argument String make, @Argument String model, @Argument String trim,
|
||||||
|
@Argument Location location) {
|
||||||
|
return this.inventoryService.addVehicle(vin, year, make, model, trim, location);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.baeldung.graphql.error.handling.exception;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class AbstractGraphQLException extends RuntimeException {
|
||||||
|
|
||||||
|
private Map<String, Object> parameters = new HashMap<>();
|
||||||
|
|
||||||
|
public AbstractGraphQLException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractGraphQLException(String message, Map<String, Object> additionParams) {
|
||||||
|
this(message);
|
||||||
|
if (additionParams != null) {
|
||||||
|
parameters = additionParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getExtensions() {
|
||||||
|
return parameters.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package com.baeldung.graphql.error.handling.exception;
|
||||||
|
|
||||||
|
import graphql.GraphQLError;
|
||||||
|
import graphql.GraphqlErrorBuilder;
|
||||||
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
||||||
|
import org.springframework.graphql.execution.ErrorType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
||||||
|
if (ex instanceof VehicleNotFoundException) {
|
||||||
|
return GraphqlErrorBuilder.newError()
|
||||||
|
.errorType(ErrorType.NOT_FOUND)
|
||||||
|
.message(ex.getMessage())
|
||||||
|
.path(env.getExecutionStepInfo().getPath())
|
||||||
|
.location(env.getField().getSourceLocation())
|
||||||
|
.build();
|
||||||
|
} else if (ex instanceof AbstractGraphQLException) {
|
||||||
|
return GraphqlErrorBuilder.newError()
|
||||||
|
.errorType(ErrorType.INTERNAL_ERROR)
|
||||||
|
.message(ex.getMessage())
|
||||||
|
.path(env.getExecutionStepInfo().getPath())
|
||||||
|
.location(env.getField().getSourceLocation())
|
||||||
|
.extensions(((AbstractGraphQLException) ex).getExtensions())
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.baeldung.graphql.error.handling.exception;
|
||||||
|
|
||||||
|
public class VehicleNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public VehicleNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,8 +15,8 @@ import java.util.*;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class InventoryService {
|
public class InventoryService {
|
||||||
private InventoryRepository inventoryRepository;
|
private final InventoryRepository inventoryRepository;
|
||||||
private LocationRepository locationRepository;
|
private final LocationRepository locationRepository;
|
||||||
|
|
||||||
public InventoryService(InventoryRepository inventoryRepository, LocationRepository locationRepository) {
|
public InventoryService(InventoryRepository inventoryRepository, LocationRepository locationRepository) {
|
||||||
this.inventoryRepository = inventoryRepository;
|
this.inventoryRepository = inventoryRepository;
|
||||||
@ -29,7 +29,7 @@ public class InventoryService {
|
|||||||
if (existingVehicle.isPresent()) {
|
if (existingVehicle.isPresent()) {
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("vin", vin);
|
params.put("vin", vin);
|
||||||
throw new VehicleAlreadyPresentException("Failed to add vehicle. Vehicle with vin " + vin + " already present.", params);
|
throw new VehicleAlreadyPresentException("Failed to add vehicle. Vehicle with vin already present.", params);
|
||||||
}
|
}
|
||||||
Vehicle vehicle = Vehicle.builder()
|
Vehicle vehicle = Vehicle.builder()
|
||||||
.vin(vin)
|
.vin(vin)
|
||||||
@ -58,10 +58,7 @@ public class InventoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Vehicle searchByVin(String vin) {
|
public Vehicle searchByVin(String vin) {
|
||||||
return this.inventoryRepository.findById(vin).orElseThrow(() -> {
|
return this.inventoryRepository.findById(vin)
|
||||||
Map<String, Object> params = new HashMap<>();
|
.orElseThrow(() -> new VehicleNotFoundException("Vehicle with vin: " + vin + " not found."));
|
||||||
params.put("vin", vin);
|
|
||||||
return new VehicleNotFoundException("Vehicle with vin " + vin + " not found.", params);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -1,12 +1,16 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {
|
||||||
|
SecurityAutoConfiguration.class,
|
||||||
|
HibernateJpaAutoConfiguration.class
|
||||||
|
})
|
||||||
public class GraphqlApplication {
|
public class GraphqlApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import org.springframework.graphql.data.method.annotation.Argument;
|
import org.springframework.graphql.data.method.annotation.Argument;
|
||||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
@ -1,23 +1,24 @@
|
|||||||
graphql:
|
server:
|
||||||
servlet:
|
port: 8081
|
||||||
mapping: /graphql
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
graphql:
|
||||||
|
schema:
|
||||||
|
locations: classpath:error-handling/graphql/
|
||||||
datasource:
|
datasource:
|
||||||
url: "jdbc:h2:mem:graphqldb"
|
url: "jdbc:h2:mem:graphqldb"
|
||||||
driverClassName: "org.h2.Driver"
|
driverClassName: "org.h2.Driver"
|
||||||
username: sa
|
username: sa
|
||||||
password:
|
password:
|
||||||
|
|
||||||
initialization-mode: always
|
initialization-mode: always
|
||||||
platform: h2
|
platform: h2
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
show-sql: true
|
show-sql: true
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.H2Dialect
|
dialect: org.hibernate.dialect.H2Dialect
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
|
globally_quoted_identifiers: true
|
||||||
|
|
||||||
h2:
|
h2:
|
||||||
console.enabled: true
|
console.enabled: true
|
@ -0,0 +1,7 @@
|
|||||||
|
insert into "location" values('07092', 'Mountainside', 'NJ');
|
||||||
|
insert into "location" values ('94118', 'San Francisco', 'CA');
|
||||||
|
insert into "location" values ('10002', 'New York', 'NY');
|
||||||
|
|
||||||
|
insert into "vehicle" ("vin", "year", "make", "model", "trim", "fk_location") values('KM8JN72DX7U587496', 2007, 'Hyundai', 'Tucson', null, '07092');
|
||||||
|
insert into "vehicle" ("vin", "year", "make", "model", "trim", "fk_location") values('JTKKU4B41C1023346', 2012, 'Toyota', 'Scion', 'Xd', '94118');
|
||||||
|
insert into "vehicle" ("vin", "year", "make", "model", "trim", "fk_location") values('1G1JC1444PZ215071', 2000, 'Chevrolet', 'CAVALIER VL', 'RS', '07092');
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.baeldung.graphql.error.handling;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
|
import org.springframework.graphql.test.tester.HttpGraphQlTester;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static graphql.ErrorType.NullValueInNonNullableField;
|
||||||
|
import static org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR;
|
||||||
|
import static org.springframework.graphql.execution.ErrorType.NOT_FOUND;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = GraphQLErrorHandlerApplication.class)
|
||||||
|
@ActiveProfiles("error-handling")
|
||||||
|
public class GraphQLErrorHandlerIntegrationTest {
|
||||||
|
|
||||||
|
private static final String GRAPHQL_TEST_REQUEST_PATH = "src/test/resources/graphql-files/request/%s_request.graphql";
|
||||||
|
private static final String GRAPHQL_TEST_RESPONSE_PATH = "src/test/resources/graphql-files/response/%s_response.json";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HttpGraphQlTester graphQlTester;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenMandatoryFieldNull_thenRespondWithResponseError() throws IOException {
|
||||||
|
String nonNullFieldScenario = "non_null_field";
|
||||||
|
|
||||||
|
graphQlTester.document(fileToRequest(nonNullFieldScenario))
|
||||||
|
.execute()
|
||||||
|
.errors()
|
||||||
|
.expect(error -> error.getErrorType() == NullValueInNonNullableField)
|
||||||
|
.verify()
|
||||||
|
.path("$.data")
|
||||||
|
.matchesJson(fileToResponse(nonNullFieldScenario));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenUnhandledException_thenRespondWithGenericError() throws IOException {
|
||||||
|
String unhandledExceptionScenario = "unhandled_exception";
|
||||||
|
|
||||||
|
graphQlTester.document(fileToRequest(unhandledExceptionScenario))
|
||||||
|
.execute()
|
||||||
|
.errors()
|
||||||
|
.expect(error -> error.getErrorType() == INTERNAL_ERROR)
|
||||||
|
.verify()
|
||||||
|
.path("$.data")
|
||||||
|
.valueIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenHandledException_thenRespondWithCustomErrorDetails() throws IOException {
|
||||||
|
String handledExceptionScenario = "handled_exception";
|
||||||
|
|
||||||
|
graphQlTester.document(fileToRequest(handledExceptionScenario))
|
||||||
|
.execute()
|
||||||
|
.errors()
|
||||||
|
.expect(error -> error.getErrorType() == NOT_FOUND
|
||||||
|
&& "Vehicle with vin: 123 not found.".equals(error.getMessage()))
|
||||||
|
.verify()
|
||||||
|
.path("$.data")
|
||||||
|
.matchesJson("{\n"
|
||||||
|
+ " \"searchByVin\": null\n"
|
||||||
|
+ " }");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenNoException_thenRespondWithNoError() throws IOException {
|
||||||
|
String noExceptionScenario = "no_exception";
|
||||||
|
|
||||||
|
graphQlTester.document(fileToRequest(noExceptionScenario))
|
||||||
|
.execute()
|
||||||
|
.path("$.data")
|
||||||
|
.matchesJson(fileToResponse(noExceptionScenario));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fileToRequest(String fileName) throws IOException {
|
||||||
|
Path path = Paths.get(String.format(GRAPHQL_TEST_REQUEST_PATH, fileName));
|
||||||
|
return new String(Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fileToResponse(String fileName) throws IOException {
|
||||||
|
Path path = Paths.get(String.format(GRAPHQL_TEST_RESPONSE_PATH, fileName));
|
||||||
|
return new String(Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
@ -1,5 +1,6 @@
|
|||||||
package com.baeldung.graphql;
|
package com.baeldung.graphql.intro;
|
||||||
|
|
||||||
|
import com.baeldung.graphql.intro.GraphqlApplication;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
@ -1,9 +1,10 @@
|
|||||||
query {
|
query {
|
||||||
searchByVin(vin: "error) {
|
searchByVin(vin: "123"){
|
||||||
vin
|
vin
|
||||||
year
|
year
|
||||||
make
|
make
|
||||||
model
|
model
|
||||||
trim
|
trim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
query {
|
||||||
|
searchByVin(vin: "KM8JN72DX7U587496"){
|
||||||
|
vin
|
||||||
|
year
|
||||||
|
make
|
||||||
|
model
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
subscription {
|
query {
|
||||||
searchByVin(vin: "75024") {
|
searchAll {
|
||||||
vin
|
vin
|
||||||
year
|
year
|
||||||
make
|
make
|
||||||
model
|
model
|
||||||
trim
|
trim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
query {
|
||||||
|
searchByLocation(zipcode: "123"){
|
||||||
|
vin
|
||||||
|
year
|
||||||
|
make
|
||||||
|
model
|
||||||
|
trim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"searchByVin": {
|
||||||
|
"vin": "KM8JN72DX7U587496",
|
||||||
|
"year": 2007,
|
||||||
|
"make": "Hyundai",
|
||||||
|
"model": "Tucson"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"searchAll": [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"vin": "JTKKU4B41C1023346",
|
||||||
|
"year": 2012,
|
||||||
|
"make": "Toyota",
|
||||||
|
"model": "Scion",
|
||||||
|
"trim": "Xd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vin": "1G1JC1444PZ215071",
|
||||||
|
"year": 2000,
|
||||||
|
"make": "Chevrolet",
|
||||||
|
"model": "CAVALIER VL",
|
||||||
|
"trim": "RS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user