diff --git a/README.md b/README.md index aae5ad1..0ad16ea 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,78 @@ public class UserService { > Bulk Operations are allowed on `Post` ,`Patch`, `Delete` and `Upsert` +#### Rpc Calls + +Supports of [rpc function calls](https://postgrest.org/en/v12/references/api/functions.html#functions-as-rpc) + +**Configuration** +Its use a PostgrestRpcClient which use the PostgrestClient + +```java +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.ouestfrance.querydsl.postgrest.PostgrestClient; +import fr.ouestfrance.querydsl.postgrest.PostgrestWebClient; +import fr.ouestfrance.querydsl.postgrest.PostgrestRpcClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class PostgrestConfiguration { + + @Bean + public PostgrestRpcClient rpcClient() { + String serviceUrl = "http://localhost:9000"; + WebClient webclient = WebClient.builder() + .baseUrl(serviceUrl) + // Here you can add any filters or default configuration you want + .build(); + + return new PostgrestRpcClient(PostgrestWebClient.of(webclient)); + } +} +``` + +then you can call your rpc method using this call + +```java +public class Example{ + private PostgrestRpcClient rpcClient; + + public List getCoordinates(){ + // call getCoordinates_v1 and expect to return a list of Coordinates + return rpcClient.exectureRpc("getCoordinates_v1", TypeUtils.parameterize(List.class, Coordinate.class)); + // CALL => ${base_url}/rpc/getCoordinates_v1 + } + + public Coordinate getCoordinate(Point point){ + // call findClosestCoordinate_v1 with body {x:?, y:?} + // expect to return a single coordinate + return rpcClient.executeRpc("findClosestCoordinate_v1", point, Coordinate.class); + // CALL => ${base_url}/rpc/findClosestCoordinate_v1 + // => with body {x: point.x, y: point.y} + } + + public SimpleCoordinate getCoordinateX(Point point){ + // call findClosestCoordinate_v1 with body {x:?, y:?} + // add Criteria that add select=x,y and z=gte.0.0 + // and return the result as a SimpleCoordinate class + return rpcClient.executeRpc("findClosestCoordinate_v1", new CoordinateCriteria(0.0), point, SimpleCoordinate.class); + // CALL => ${base_url}/rpc/findClosestCoordinate_v1?z=gte.0.0&select=x,y + // => with body {x: point.x, y: point.y} + } + + + @Select({"x", "y"}) + record CoordinateCriteria( + @FilterField(key = "z", operation = FilterOperation.GTE.class) + private Float z + ){} + + record SimpleCoordinate(Float x, Float y){} +} +``` + ## Need Help ? If you need help with the library please start a new thread QA / Issue on github diff --git a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java index 4a8dfb6..95ddfb7 100644 --- a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java +++ b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java @@ -6,6 +6,7 @@ import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -15,6 +16,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.lang.reflect.Type; import java.net.URI; import java.util.List; import java.util.Map; @@ -66,7 +68,7 @@ public RangeResponse search(String resource, Map> pa } @Override - public BulkResponse post(String resource, Map> params, List value, Map> headers, Class clazz) { + public BulkResponse post(String resource, Map> params, Object value, Map> headers, Class clazz) { ResponseEntity> response = restTemplate.exchange( getUri(resource, params), HttpMethod.POST, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz)); @@ -95,6 +97,12 @@ public List count(String resource, Map> map) { getUri(resource, map), HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), listRef(CountItem.class)).getBody(); } + @Override + public V rpc(String rpcName, Map> map, Object body, Type type) { + return (V) restTemplate.exchange( + getUri(rpcName, map), HttpMethod.POST, new HttpEntity<>(body, new HttpHeaders()), ParameterizedTypeReference.forType(type)).getBody(); + } + /** * Convert map to MultiValueMap diff --git a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java index c842af1..4007b8d 100644 --- a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java +++ b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java @@ -8,6 +8,7 @@ import fr.ouestfrance.querydsl.postgrest.model.Page; import fr.ouestfrance.querydsl.postgrest.model.Pageable; import lombok.SneakyThrows; +import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,13 +32,16 @@ class PostgrestRestTemplateRepositoryTest { private PostgrestRepository repository; + private PostgrestRpcClient rpcClient; @BeforeEach void beforeEach(MockServerClient client) { client.reset(); RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClients.createDefault())); - repository = new PostRepository(PostgrestRestTemplate.of(restTemplate, "http://localhost:8007")); + PostgrestRestTemplate postgrestRestTemplate = PostgrestRestTemplate.of(restTemplate, "http://localhost:8007"); + repository = new PostRepository(postgrestRestTemplate); + rpcClient = new PostgrestRpcClient(postgrestRestTemplate); } @Test @@ -185,6 +189,37 @@ void shouldDeleteBulkPost(MockServerClient client) { assertTrue(result.isEmpty()); } + + @Test + void shouldCallRpc(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1")) + .respond(jsonResponse(""" + {"id": 1, "title": "test"} + """)); + + Post result = rpcClient.executeRpc("testV1", null, Post.class); + assertNotNull(result); + System.out.println(result); + } + + + @Test + void shouldCallRpcResultIsList(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1")) + .respond(jsonResponse(""" + [{"id": 1, "title": "test"}] + """)); + + List result = rpcClient.executeRpc("testV1", null, TypeUtils.parameterize(List.class, Post.class)); + assertNotNull(result); + System.out.println(result); + } + + private HttpResponse jsonResponse(String content) { + return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) + .withBody(content); + } + private HttpResponse jsonFileResponse(String resourceFileName) { return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) .withBody(jsonOf(resourceFileName)); diff --git a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcTest.java b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcTest.java new file mode 100644 index 0000000..f2f06d8 --- /dev/null +++ b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcTest.java @@ -0,0 +1,75 @@ +package fr.ouestfrance.querydsl.postgrest; + +import fr.ouestfrance.querydsl.postgrest.app.Post; +import fr.ouestfrance.querydsl.postgrest.app.PostRequestWithSelect; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.junit.jupiter.MockServerSettings; +import org.mockserver.model.HttpRequest; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +import static fr.ouestfrance.querydsl.postgrest.TestUtils.jsonResponse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@MockServerSettings(ports = 8007) +class PostgrestRpcTest { + + private PostgrestRpcClient rpcClient; + + @BeforeEach + void beforeEach(MockServerClient client) { + client.reset(); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClients.createDefault())); + PostgrestRestTemplate postgrestRestTemplate = PostgrestRestTemplate.of(restTemplate, "http://localhost:8007"); + rpcClient = new PostgrestRpcClient(postgrestRestTemplate); + } + + @Test + void shouldCallRpc(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1")) + .respond(jsonResponse(""" + {"id": 1, "title": "test"} + """)); + Post result = rpcClient.executeRpc("testV1", Post.class); + assertNotNull(result); + } + + @Test + void shouldCallRpcWithCriteria(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1") + .withQueryStringParameter("userId", "eq.1") + .withQueryStringParameter("select", "title,userId")) + .respond(jsonResponse(""" + {"id": 1, "title": "test"} + """)); + + PostRequestWithSelect criteria = new PostRequestWithSelect(); + criteria.setUserId(1); + Post result = rpcClient.executeRpc("testV1", criteria,null, Post.class); + assertNotNull(result); + System.out.println(result); + } + + @Test + void shouldCallRpcWithCriteriaResultIsList(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1") + .withQueryStringParameter("userId", "eq.1") + .withQueryStringParameter("select", "title,userId")) + .respond(jsonResponse(""" + [{"id": 1, "title": "test"}] + """)); + + PostRequestWithSelect criteria = new PostRequestWithSelect(); + criteria.setUserId(1); + List result = rpcClient.executeRpc("testV1", criteria, null, TypeUtils.parameterize(List.class, Post.class)); + assertNotNull(result); + System.out.println(result); + } +} diff --git a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java new file mode 100644 index 0000000..7508593 --- /dev/null +++ b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java @@ -0,0 +1,29 @@ +package fr.ouestfrance.querydsl.postgrest; + +import lombok.SneakyThrows; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.MediaType; +import shaded_package.org.apache.commons.io.IOUtils; + +import java.nio.charset.Charset; + +import static org.mockserver.model.HttpResponse.response; + +public class TestUtils { + + public static HttpResponse jsonResponse(String content) { + return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) + .withBody(content); + } + + + public static HttpResponse jsonFileResponse(String resourceFileName) { + return response().withContentType(MediaType.APPLICATION_JSON) + .withBody(jsonOf(resourceFileName)); + } + + @SneakyThrows + public static String jsonOf(String name) { + return IOUtils.resourceToString(name, Charset.defaultCharset(), TestUtils.class.getClassLoader()); + } +} diff --git a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java new file mode 100644 index 0000000..4695445 --- /dev/null +++ b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java @@ -0,0 +1,22 @@ +package fr.ouestfrance.querydsl.postgrest.app; + +import fr.ouestfrance.querydsl.FilterField; +import fr.ouestfrance.querydsl.FilterOperation; +import fr.ouestfrance.querydsl.postgrest.annotations.Select; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Select({"title", "userId"}) +public class PostRequestWithSelect { + + @FilterField + private Integer userId; + @FilterField(operation = FilterOperation.NEQ.class) + private Integer id; +} diff --git a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java index bdbccb3..8efc41c 100644 --- a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java +++ b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java @@ -6,6 +6,7 @@ import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -14,6 +15,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.WebClient; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.Optional; @@ -83,9 +86,29 @@ public List count(String resource, Map> params) return Optional.ofNullable(response).map(HttpEntity::getBody).orElse(List.of()); } + @Override + public V rpc(String rpcName, Map> params, Object body, Type clazz) { + WebClient.RequestBodySpec request = webClient.post().uri(uriBuilder -> { + uriBuilder.path(rpcName); + uriBuilder.queryParams(toMultiMap(params)); + return uriBuilder.build(); + }); + if(body != null){ + request.bodyValue(body); + } + Object result = request + .retrieve() + .bodyToMono(ParameterizedTypeReference.forType(clazz)) + .block(); + if(result != null) { + return (V) result; + } + return null; + } + @Override - public BulkResponse post(String resource, Map> params, List value, Map> headers, Class clazz) { + public BulkResponse post(String resource, Map> params, Object value, Map> headers, Class clazz) { ResponseEntity> response = webClient.post().uri(uriBuilder -> { uriBuilder.path(resource); uriBuilder.queryParams(toMultiMap(params)); @@ -119,7 +142,8 @@ public BulkResponse delete(String resource, Map> par uriBuilder.path(resource); uriBuilder.queryParams(toMultiMap(params)); return uriBuilder.build(); - }).headers(httpHeaders -> safeAdd(headers, httpHeaders)) + }) + .headers(httpHeaders -> safeAdd(headers, httpHeaders)) .retrieve() .toEntity(listRef(clazz)).block(); return toBulkResponse(response); diff --git a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java index 9b3c846..97e1e4e 100644 --- a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java +++ b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java @@ -8,9 +8,12 @@ import fr.ouestfrance.querydsl.postgrest.model.Range; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.reflect.TypeUtils; import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; import org.mockserver.integration.ClientAndServer; import org.mockserver.junit.jupiter.MockServerSettings; +import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import org.mockserver.model.MediaType; import org.springframework.web.reactive.function.client.WebClient; @@ -21,6 +24,7 @@ import java.util.ArrayList; import java.util.List; +import static fr.ouestfrance.querydsl.postgrest.TestUtils.jsonFileResponse; import static org.junit.jupiter.api.Assertions.*; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; @@ -29,9 +33,10 @@ @Slf4j class PostgrestWebClientRepositoryTest { - private final PostgrestRepository repository = new PostRepository(PostgrestWebClient.of(WebClient.builder() + private final PostgrestWebClient client = PostgrestWebClient.of(WebClient.builder() .baseUrl("http://localhost:8007/") - .build())); + .build()); + private final PostgrestRepository repository = new PostRepository(client); @Test @@ -178,15 +183,8 @@ void shouldDeleteBulkPost(ClientAndServer client) { assertTrue(result.isEmpty()); } - private HttpResponse jsonFileResponse(String resourceFileName) { - return response().withContentType(MediaType.APPLICATION_JSON) - .withBody(jsonOf(resourceFileName)); - } - @SneakyThrows - private String jsonOf(String name) { - return IOUtils.resourceToString(name, Charset.defaultCharset(), getClass().getClassLoader()); - } + @Test @@ -213,6 +211,4 @@ void shouldValidateRangeRequest(ClientAndServer client) { Page oldWaySearch = repository.search(oldWayRangeRequest, Pageable.ofSize(6)); assertNotNull(oldWaySearch); } - - } diff --git a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRpcTest.java b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRpcTest.java new file mode 100644 index 0000000..037be96 --- /dev/null +++ b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRpcTest.java @@ -0,0 +1,73 @@ +package fr.ouestfrance.querydsl.postgrest; + +import fr.ouestfrance.querydsl.postgrest.app.Post; +import fr.ouestfrance.querydsl.postgrest.app.PostRequestWithSelect; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.junit.jupiter.MockServerSettings; +import org.mockserver.model.HttpRequest; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; + +import static fr.ouestfrance.querydsl.postgrest.TestUtils.jsonResponse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@MockServerSettings(ports = 8007) +@Slf4j +class PostgrestWebClientRpcTest { + + private final PostgrestWebClient client = PostgrestWebClient.of(WebClient.builder() + .baseUrl("http://localhost:8007/") + .build()); + private final PostgrestRpcClient rpcClient = new PostgrestRpcClient(client); + + @BeforeEach + void beforeEach(MockServerClient client) { + client.reset(); + } + + @Test + void shouldCallRpc(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1")) + .respond(jsonResponse(""" + {"id": 1, "title": "test"} + """)); + Post result = rpcClient.executeRpc("testV1", Post.class); + assertNotNull(result); + } + + @Test + void shouldCallRpcWithCriteria(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1") + .withQueryStringParameter("userId", "eq.1") + .withQueryStringParameter("select", "title,userId")) + .respond(jsonResponse(""" + {"id": 1, "title": "test"} + """)); + + PostRequestWithSelect criteria = new PostRequestWithSelect(); + criteria.setUserId(1); + Post result = rpcClient.executeRpc("testV1", criteria, null, Post.class); + assertNotNull(result); + } + + @Test + void shouldCallRpcWithCriteriaResultIsList(MockServerClient client) { + client.when(HttpRequest.request().withPath("/rpc/testV1") + .withQueryStringParameter("userId", "eq.1") + .withQueryStringParameter("select", "title,userId")) + .respond(jsonResponse(""" + [{"id": 1, "title": "test"}] + """)); + + PostRequestWithSelect criteria = new PostRequestWithSelect(); + criteria.setUserId(1); + List result = rpcClient.executeRpc("testV1", criteria, null, TypeUtils.parameterize(List.class, Post.class)); + assertNotNull(result); + } + +} diff --git a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java new file mode 100644 index 0000000..7508593 --- /dev/null +++ b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/TestUtils.java @@ -0,0 +1,29 @@ +package fr.ouestfrance.querydsl.postgrest; + +import lombok.SneakyThrows; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.MediaType; +import shaded_package.org.apache.commons.io.IOUtils; + +import java.nio.charset.Charset; + +import static org.mockserver.model.HttpResponse.response; + +public class TestUtils { + + public static HttpResponse jsonResponse(String content) { + return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) + .withBody(content); + } + + + public static HttpResponse jsonFileResponse(String resourceFileName) { + return response().withContentType(MediaType.APPLICATION_JSON) + .withBody(jsonOf(resourceFileName)); + } + + @SneakyThrows + public static String jsonOf(String name) { + return IOUtils.resourceToString(name, Charset.defaultCharset(), TestUtils.class.getClassLoader()); + } +} diff --git a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java new file mode 100644 index 0000000..9de6c52 --- /dev/null +++ b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/app/PostRequestWithSelect.java @@ -0,0 +1,25 @@ +package fr.ouestfrance.querydsl.postgrest.app; + +import fr.ouestfrance.querydsl.FilterField; +import fr.ouestfrance.querydsl.FilterOperation; +import fr.ouestfrance.querydsl.postgrest.annotations.Select; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Select({"title", "userId"}) +public class PostRequestWithSelect { + + @FilterField + private Integer userId; + @FilterField(operation = FilterOperation.NEQ.class) + private Integer id; +} diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java index f6390ae..63efd8f 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java @@ -4,6 +4,7 @@ import fr.ouestfrance.querydsl.postgrest.model.CountItem; import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; @@ -35,7 +36,7 @@ RangeResponse search(String resource, Map> params, * @param clazz type of return * @return list of inserted objects */ - BulkResponse post(String resource, Map> queryParams, List value, Map> headers, Class clazz); + BulkResponse post(String resource, Map> queryParams, Object value, Map> headers, Class clazz); /** * Patch data @@ -70,4 +71,16 @@ RangeResponse search(String resource, Map> params, * @return list of count items */ List count(String resource, Map> map); + + /** + * Call RPC + * + * @param rpcName rpc name + * @param params query params + * @param body body request to send + * @param type class of return + * @param type of return object + * @return MultiValueMap + */ + V rpc(String rpcName, Map> params, Object body, Type type); } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java index 592cbdc..2874f73 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java @@ -3,22 +3,22 @@ import fr.ouestfrance.querydsl.postgrest.annotations.Header; import fr.ouestfrance.querydsl.postgrest.annotations.OnConflict; import fr.ouestfrance.querydsl.postgrest.annotations.PostgrestConfiguration; -import fr.ouestfrance.querydsl.postgrest.annotations.Select; import fr.ouestfrance.querydsl.postgrest.model.*; import fr.ouestfrance.querydsl.postgrest.model.exceptions.MissingConfigurationException; import fr.ouestfrance.querydsl.postgrest.model.exceptions.PostgrestRequestException; -import fr.ouestfrance.querydsl.postgrest.model.impl.CompositeFilter; import fr.ouestfrance.querydsl.postgrest.model.impl.CountFilter; import fr.ouestfrance.querydsl.postgrest.model.impl.OrderFilter; import fr.ouestfrance.querydsl.postgrest.model.impl.SelectFilter; import fr.ouestfrance.querydsl.postgrest.services.BulkExecutorService; import fr.ouestfrance.querydsl.postgrest.services.ext.PostgrestQueryProcessorService; +import fr.ouestfrance.querydsl.postgrest.utils.FilterUtils; import fr.ouestfrance.querydsl.service.ext.QueryDslProcessorService; import java.lang.reflect.ParameterizedType; import java.util.*; import static fr.ouestfrance.querydsl.postgrest.annotations.Header.Method.*; +import static fr.ouestfrance.querydsl.postgrest.utils.FilterUtils.toMap; /** * Postgrest repository implementation @@ -170,29 +170,6 @@ public BulkResponse delete(Object criteria, BulkOptions options) { options); } - /** - * Transform a filter list to map of queryString - * - * @param filters list of filters - * @return map of query strings - */ - private Map> toMap(List filters) { - Map> map = new LinkedHashMap<>(); - filters.forEach(x -> { - // If filter is an "and" with the same keys, then we decompose it and transform it to filter list - if (x instanceof CompositeFilter compositeFilter && compositeFilter.getKey().equals("and") && - compositeFilter.getFilters().stream().map(Filter::getKey).distinct().count() == 1) { - for (Filter filter : compositeFilter.getFilters()) { - map.computeIfAbsent(filter.getKey(), key -> new ArrayList<>()).add(filter.getFilterString()); - } - } else { - map.computeIfAbsent(x.getKey(), key -> new ArrayList<>()).add(x.getFilterString()); - } - }); - return map; - } - - /** * Extract selection on criteria and class * @@ -200,18 +177,7 @@ private Map> toMap(List filters) { * @return attributes */ private Optional getSelects(Object criteria) { - List attributes = new ArrayList<>(); - Select[] clazzAnnotation = getClass().getAnnotationsByType(Select.class); - if (clazzAnnotation.length > 0) { - attributes.addAll(Arrays.stream(clazzAnnotation).map(x -> new SelectFilter.Attribute(x.alias(), x.value())).toList()); - } - if (criteria != null) { - Select[] criteriaAnnotation = criteria.getClass().getAnnotationsByType(Select.class); - if (criteriaAnnotation.length > 0) { - attributes.addAll(Arrays.stream(criteriaAnnotation).map(x -> new SelectFilter.Attribute(x.alias(), x.value())).toList()); - } - } - return Optional.of(attributes) + return Optional.of(FilterUtils.getSelectAttributes(this, criteria)) .filter(x -> !x.isEmpty()) .map(SelectFilter::of); } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcClient.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcClient.java new file mode 100644 index 0000000..994a410 --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRpcClient.java @@ -0,0 +1,79 @@ +package fr.ouestfrance.querydsl.postgrest; + +import fr.ouestfrance.querydsl.postgrest.model.Filter; +import fr.ouestfrance.querydsl.postgrest.model.impl.SelectFilter; +import fr.ouestfrance.querydsl.postgrest.services.ext.PostgrestQueryProcessorService; +import fr.ouestfrance.querydsl.postgrest.utils.FilterUtils; +import fr.ouestfrance.querydsl.service.ext.QueryDslProcessorService; +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class PostgrestRpcClient { + + private static final String RPC = "rpc/"; + private final PostgrestClient client; + private final QueryDslProcessorService processorService = new PostgrestQueryProcessorService(); + + /** + * Execute a rpc call without body + * + * @param rpcName rpc name + * @param type class of return + * @param type of the return object + * @return response + */ + public V executeRpc(String rpcName, Type type) { + return executeRpc(rpcName, null, type); + } + + /** + * Execute a rpc call + * + * @param rpcName rpc name + * @param body body request to send + * @param type class of return + * @param type of return object + * @return response + */ + public V executeRpc(String rpcName, Object body, Type type) { + return executeRpc(rpcName, null, body, type); + } + + + /** + * Execute a rpc call + * + * @param rpcName rpc name + * @param body body request to send + * @param type class of return + * @param type of return object + * @return response + */ + public V executeRpc(String rpcName, Object criteria, Object body, Type type) { + // List filters + List queryParams = processorService.process(criteria); + // Extract selection + getSelects(criteria).ifPresent(queryParams::add); + + return client.rpc(RPC + rpcName, FilterUtils.toMap(queryParams), body, type); + } + + + /** + * Extract selection on criteria and class + * + * @param criteria search criteria + * @return attributes + */ + private Optional getSelects(Object criteria) { + return Optional.of(FilterUtils.getSelectAttributes(criteria)) + .filter(x -> !x.isEmpty()) + .map(SelectFilter::only); + } + +} diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java index 1107e5f..4b11b08 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java @@ -6,6 +6,8 @@ import fr.ouestfrance.querydsl.postgrest.model.Pageable; import fr.ouestfrance.querydsl.postgrest.model.exceptions.PostgrestRequestException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; import java.util.Optional; @@ -178,4 +180,5 @@ default BulkResponse delete(Object criteria) { */ BulkResponse delete(Object criteria, BulkOptions options); + } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/annotations/Select.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/annotations/Select.java index 0522f4a..a63d878 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/annotations/Select.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/annotations/Select.java @@ -16,7 +16,7 @@ * Selection * @return string representation of the selection */ - String value(); + String[] value(); /** * Select the value as a specific alias name diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/builders/QueryFilterVisitor.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/builders/QueryFilterVisitor.java index 10be055..441ec06 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/builders/QueryFilterVisitor.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/builders/QueryFilterVisitor.java @@ -4,6 +4,8 @@ import fr.ouestfrance.querydsl.postgrest.model.Sort; import fr.ouestfrance.querydsl.postgrest.model.impl.*; +import java.lang.reflect.Array; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -40,8 +42,8 @@ public void visit(QueryFilter filter) { public void visit(OrderFilter filter) { builder.append(filter.getSort().getOrders().stream().map( x -> x.getProperty() - + (Sort.Direction.ASC.equals(x.getDirection()) ? EMPTY_STRING : DOT + "desc") - + (switch (x.getNullHandling()) { + + (Sort.Direction.ASC.equals(x.getDirection()) ? EMPTY_STRING : DOT + "desc") + + (switch (x.getNullHandling()) { case NATIVE -> EMPTY_STRING; case NULLS_FIRST -> DOT + "nullsfirst"; case NULLS_LAST -> DOT + "nullslast"; @@ -54,9 +56,13 @@ public void visit(OrderFilter filter) { * @param filter select filter */ public void visit(SelectFilter filter) { - builder.append("*,"); - builder.append(filter.getSelectAttributes().stream().map( - x -> x.getAlias().isEmpty() ? x.getValue() : x.getAlias() + ":" + x.getValue()) + if (filter.isAddAll()) { + builder.append("*,"); + } + builder.append(filter.getSelectAttributes().stream() + .filter(x -> x.getValue() != null && x.getValue().length > 0) + .map( + x -> x.getAlias().isEmpty() ? String.join(",", x.getValue()) : x.getAlias() + ":" + Arrays.stream(x.getValue()).findFirst().get()) .collect(Collectors.joining(","))); } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/SelectFilter.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/SelectFilter.java index 2fef972..30a28a2 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/SelectFilter.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/SelectFilter.java @@ -4,6 +4,7 @@ import fr.ouestfrance.querydsl.postgrest.builders.QueryFilterVisitor; import fr.ouestfrance.querydsl.postgrest.model.Filter; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,7 +14,7 @@ * Select filter allow to describe a selection */ @Getter -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class SelectFilter implements Filter, FilterVisitor { /** @@ -23,7 +24,8 @@ public class SelectFilter implements Filter, FilterVisitor { /** * alias */ - private final List selectAttributes; + private List selectAttributes; + private boolean addAll; /** * Create select filter from embedded resources @@ -32,7 +34,19 @@ public class SelectFilter implements Filter, FilterVisitor { * @return select filter */ public static Filter of(List selectAttributes) { - return new SelectFilter(selectAttributes); + return new SelectFilter(selectAttributes, true); + } + + public static Filter only(List selectAttributes) { + return new SelectFilter(selectAttributes, false); + } + + public Filter append(List selectAttributes) { + if(selectAttributes == null || selectAttributes.isEmpty()) { + return this; + } + this.selectAttributes.addAll(selectAttributes); + return this; } @Override @@ -59,7 +73,7 @@ public static class Attribute { /** * value selected */ - private final String value; + private final String[] value; } } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/utils/FilterUtils.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/utils/FilterUtils.java new file mode 100644 index 0000000..7db212c --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/utils/FilterUtils.java @@ -0,0 +1,58 @@ +package fr.ouestfrance.querydsl.postgrest.utils; + +import fr.ouestfrance.querydsl.postgrest.annotations.Select; +import fr.ouestfrance.querydsl.postgrest.model.Filter; +import fr.ouestfrance.querydsl.postgrest.model.impl.CompositeFilter; +import fr.ouestfrance.querydsl.postgrest.model.impl.SelectFilter; +import fr.ouestfrance.querydsl.postgrest.model.impl.SelectFilter.Attribute; + +import java.util.*; + +public class FilterUtils { + + + private static final String AND = "and"; + + /** + * Get attributes from objects + * @param objects objects to scan + * @return list of select attributes + */ + public static List getSelectAttributes(Object... objects) { + List attributes = new ArrayList<>(); + if(objects == null) { + return attributes; + } + for (Object object : objects) { + if(object != null) { + Select[] clazzAnnotation = object.getClass().getAnnotationsByType(Select.class); + if (clazzAnnotation.length > 0) { + attributes.addAll(Arrays.stream(clazzAnnotation).map(x -> new SelectFilter.Attribute(x.alias(), x.value())).toList()); + } + } + } + return attributes; + } + + /** + * Transform a filter list to map of queryString + * + * @param filters list of filters + * @return map of query strings + */ + public static Map> toMap(List filters) { + Map> map = new LinkedHashMap<>(); + filters.forEach(x -> { + // If filter is an "and" with the same keys, then we decompose it and transform it to filter list + if (x instanceof CompositeFilter compositeFilter && AND.equals(compositeFilter.getKey()) && + compositeFilter.getFilters().stream().map(Filter::getKey).distinct().count() == 1) { + for (Filter filter : compositeFilter.getFilters()) { + map.computeIfAbsent(filter.getKey(), key -> new ArrayList<>()).add(filter.getFilterString()); + } + } else { + map.computeIfAbsent(x.getKey(), key -> new ArrayList<>()).add(x.getFilterString()); + } + }); + return map; + } +}