From 6d02efeac37c81b05926928bdf5a846846f0bb7d Mon Sep 17 00:00:00 2001 From: Sourav Roy Date: Tue, 16 Jan 2024 21:28:22 +0000 Subject: [PATCH] Api to execute custom query --- pom.xml | 4 + .../exception/GlobalExceptionHandler.java | 31 +++++++ .../db2rest/rest/read/ReadController.java | 13 ++- .../homihq/db2rest/rest/read/ReadService.java | 10 ++- .../db2rest/rest/read/model/QueryRequest.java | 19 +++++ .../db2rest/rest/PgReadControllerTest.java | 84 ++++++++++++++++--- 6 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/homihq/db2rest/rest/read/model/QueryRequest.java diff --git a/pom.xml b/pom.xml index a36be1b0..d7f99bb6 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,10 @@ org.springframework.boot spring-boot-starter-webflux + + org.springframework.boot + spring-boot-starter-validation + com.ibm.db2 diff --git a/src/main/java/com/homihq/db2rest/exception/GlobalExceptionHandler.java b/src/main/java/com/homihq/db2rest/exception/GlobalExceptionHandler.java index 3fb0b326..72905ac9 100644 --- a/src/main/java/com/homihq/db2rest/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/homihq/db2rest/exception/GlobalExceptionHandler.java @@ -1,9 +1,40 @@ package com.homihq.db2rest.exception; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, WebRequest request) { + var body = new LinkedHashMap<>(); + body.put("type", "https://github.com/kdhrubo/db2rest/invalid-arguments"); + body.put("title", "Invalid arguments in the request"); + body.put("status", status.value()); + var errors = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> Map.of(error.getField(), Objects.requireNonNull(error.getDefaultMessage()))) + .toList(); + body.put("detail", errors); + body.put("instance", ((ServletWebRequest) request).getRequest().getRequestURI()); + body.put("errorCategory", "Invalid-Arguments"); + body.put("timestamp", Instant.now()); + return new ResponseEntity<>(body, headers, status); + } + } diff --git a/src/main/java/com/homihq/db2rest/rest/read/ReadController.java b/src/main/java/com/homihq/db2rest/rest/read/ReadController.java index 900b877b..1813c8f2 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/ReadController.java +++ b/src/main/java/com/homihq/db2rest/rest/read/ReadController.java @@ -1,12 +1,17 @@ package com.homihq.db2rest.rest.read; +import com.homihq.db2rest.rest.read.model.QueryRequest; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import static org.springframework.web.bind.ServletRequestUtils.*; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -39,7 +44,11 @@ public Object findByJoinTable(@PathVariable String tableName, return readService.findAll(schemaName, tableName,select, filter, pageable, sort); } - - + @PostMapping(value = "/query", consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findByCustomQuery(@RequestBody @Valid QueryRequest queryRequest) { + log.debug("Execute SQL statement {} with params {}", queryRequest.getSql(), queryRequest.getParams()); + return ResponseEntity.ok(readService.findByCustomQuery(queryRequest)); + } } diff --git a/src/main/java/com/homihq/db2rest/rest/read/ReadService.java b/src/main/java/com/homihq/db2rest/rest/read/ReadService.java index 528fc138..50a2bfd3 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/ReadService.java +++ b/src/main/java/com/homihq/db2rest/rest/read/ReadService.java @@ -1,6 +1,7 @@ package com.homihq.db2rest.rest.read; import com.homihq.db2rest.rest.read.helper.*; +import com.homihq.db2rest.rest.read.model.QueryRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -46,8 +47,9 @@ public Object findAll(String schemaName, String tableName, String select, String } - - - - + Object findByCustomQuery(QueryRequest queryRequest) { + return queryRequest.isSingle() ? + namedParameterJdbcTemplate.queryForMap(queryRequest.getSql(), queryRequest.getParams()) : + namedParameterJdbcTemplate.queryForList(queryRequest.getSql(), queryRequest.getParams()); + } } diff --git a/src/main/java/com/homihq/db2rest/rest/read/model/QueryRequest.java b/src/main/java/com/homihq/db2rest/rest/read/model/QueryRequest.java new file mode 100644 index 00000000..6ef5457b --- /dev/null +++ b/src/main/java/com/homihq/db2rest/rest/read/model/QueryRequest.java @@ -0,0 +1,19 @@ +package com.homihq.db2rest.rest.read.model; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +public class QueryRequest { + + @NotEmpty(message = "Sql statement cannot be empty") + private String sql; + + private Map params; + + private boolean isSingle; +} diff --git a/src/test/java/com/homihq/db2rest/rest/PgReadControllerTest.java b/src/test/java/com/homihq/db2rest/rest/PgReadControllerTest.java index 3d1c1da2..eec25127 100644 --- a/src/test/java/com/homihq/db2rest/rest/PgReadControllerTest.java +++ b/src/test/java/com/homihq/db2rest/rest/PgReadControllerTest.java @@ -1,20 +1,18 @@ package com.homihq.db2rest.rest; import com.homihq.db2rest.PostgreSQLBaseIntegrationTest; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; - -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.restdocs.request.RequestDocumentation.*; + +import static org.hamcrest.Matchers.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + class PgReadControllerTest extends PostgreSQLBaseIntegrationTest { @Test @@ -27,4 +25,70 @@ void findAllFilms() throws Exception { .andDo(print()) .andDo(document("pg-get-all-films")); } + + @Test + @DisplayName("Query returns single result") + void query_returns_single_result() throws Exception { + var json = """ + { + "sql": "SELECT FIRST_NAME,LAST_NAME FROM ACTOR WHERE ACTOR_ID = :id", + "params" : { + "id" : 1 + }, + "single" : true + } + """; + + mockMvc.perform(post("/query").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8") + .content(json).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.first_name", equalTo("PENELOPE"))) + .andDo(print()) + .andDo(document("pg-create-a-film")); + } + + @Test + @DisplayName("Query returns list of results") + void query_returns_list_of_results() throws Exception { + var json = """ + { + "sql": "SELECT FIRST_NAME,LAST_NAME FROM ACTOR WHERE ACTOR_ID IN (:id1, :id2)", + "params" : { + "id1" : 1, + "id2" : 2 + }, + "single" : false + } + """; + + mockMvc.perform(post("/query").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8") + .content(json).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.*", hasSize(2))) + .andExpect(jsonPath("$[0].first_name", equalTo("PENELOPE"))) + .andExpect(jsonPath("$[1].last_name", equalTo("WAHLBERG"))) + .andDo(print()) + .andDo(document("pg-create-a-film")); + } + + @Test + @DisplayName("Query returns 400 bad request error") + void query_returns_400_bad_request() throws Exception { + var json = """ + { + "sql": "", + "params" : { + "id1" : 1 + }, + "single" : false + } + """; + + mockMvc.perform(post("/query").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8") + .content(json).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status", is(400))) + .andDo(print()) + .andDo(document("pg-create-a-film")); + } }