diff --git a/README.md b/README.md index 28e808d4..24c9e2e8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > Don't write any database access code, Install DB2Rest instead. DB2Rest is an [Apache 2.0 Licensed](https://github.com/kdhrubo/db2rest/blob/master/LICENSE) open-source low-code middleware that provides secure and blazing fast data access layer over -your existing or new databases. You can connect to the most widely used databases like PostgreSQL, MySQL, Oracle, SQL Server, MongoDB to build a REST API in minutes without writing any code. +your existing or new databases. You can connect to the most widely used databases like PostgreSQL, MySQL, Oracle, SQL Server, and MongoDB to build a REST API in minutes without writing any code. You can now focus on building business logic and beautiful user interfaces at speed. @@ -25,14 +25,14 @@ You can now focus on building business logic and beautiful user interfaces at sp -# How it works? +# How does it work? ![DB2Rest- How it works?](assets/db2rest-hiw.png "DB2Rest") The diagram above shows an application architecture with DB2Rest. DB2Rest provides secure access to the database as REST API within seconds of installation/deployment. -The business logic can be written in your favorite technology frameworks for Java, PHP, Node, .NET or using any serverless framework. The business logic layer uses the database access layer (DBAL) provided -by DB2Rest to query and modify data. The user experience layer can be developed using popular front-end frameworks or low code/no-code platforms. This layer can make use of the business logic layer or directly access secure data layer provided by DB2Rest. +The business logic can be written in your favorite technology frameworks for Java, PHP, Node, .NET, or using any serverless framework. The business logic layer uses the database access layer (DBAL) provided +by DB2Rest to query and modify data. The user experience layer can be developed using popular front-end frameworks or low-code/no-code platforms. This layer can make use of the business logic layer or directly access secure data layer provided by DB2Rest. ## Benefits @@ -40,11 +40,11 @@ by DB2Rest to query and modify data. The user experience layer can be developed - Accelerate application development by 30x. - Unlock databases - secure REST API access for legacy data. - Blazing fast - No ORM, Single SQL Statement, 1 Database round-trip, does not use code generation. - - Support for advanced custom queries, bulk data insert, remote stored procedure calls. + - Support for advanced custom queries, bulk data insert, and remote stored procedure calls. - Best practices for transaction management, connection pooling, encryption, security - RBAC / data entitlement. - - Deploy and run anywhere - on premise, VM, Kubernetes, any cloud. + - Deploy and run anywhere - on-premise, VM, Kubernetes, or any cloud. - Zero downtime - adjusts to your evolving database schema. - - Compatible with Devops processes. + - Compatible with DevOps processes. # Installation @@ -66,7 +66,7 @@ Open JDK can be downloaded from [here](https://jdk.java.net/21/). This article f Now that you have successfully downloaded, installed, and verified Java 21, the next step is to get DB2Rest. DB2Rest is shipped as a single executable Java Archive or jar file So it's super easy to get up and running under a minute. -To download the latest edition(v-0.0.8) of DB2Rest click [here](https://www.docker.com/get-started/). +To download the latest edition(v-0.0.8) of DB2Rest click [here](https://download.db2rest.com/db2rest-0.0.8.jar). ### 3. Run DB2Rest. 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")); + } }