From 117f2c3233301021ab993f564d35a2d30a04bf36 Mon Sep 17 00:00:00 2001 From: Vignesh <125984866+Vignesh-kalyanasundaram@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:02:06 +0530 Subject: [PATCH] CIRCSTORE- 514 Implement Post API to fetch print Event Details (#471) * CIRCSTORE-514 Adding a plain index for requestId field of jsonb column * CIRCSTORE-514 Adding a new endpoint to fetch request print details * CIRCSTORE-514 Adding new endpoint in module descriptor * CIRCSTORE-514 Adding test cases, example json files * CIRCSTORE-514 Adding try catch and loggers --- descriptors/ModuleDescriptor-template.json | 17 ++- .../examples/print-events-status-request.json | 6 + .../print-events-status-response.json | 7 + .../print-events-status-responses.json | 19 +++ ramls/print-events-status-request.json | 17 +++ ramls/print-events-status-response.json | 28 ++++ ramls/print-events-status-responses.json | 23 ++++ ramls/print-events-storage.raml | 25 ++++ .../org/folio/rest/impl/PrintEventsApi.java | 9 ++ .../org/folio/service/PrintEventsService.java | 81 ++++++++++++ .../templates/db_scripts/schema.json | 12 +- .../folio/rest/api/PrintEventsAPITest.java | 122 +++++++++++++++++- 12 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 ramls/examples/print-events-status-request.json create mode 100644 ramls/examples/print-events-status-response.json create mode 100644 ramls/examples/print-events-status-responses.json create mode 100644 ramls/print-events-status-request.json create mode 100644 ramls/print-events-status-response.json create mode 100644 ramls/print-events-status-responses.json diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index f14ed302..f4f17887 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -657,6 +657,15 @@ "permissionsRequired": [ "print-events-storage.print-events-entry.item.post" ] + }, + { + "methods": [ + "POST" + ], + "pathPattern": "/print-events-storage/print-events-status", + "permissionsRequired": [ + "print-events-storage.print-events-status.item.post" + ] } ] }, @@ -697,6 +706,11 @@ "displayName": "print events storage - save print event logs", "description": "save print event log in storage" }, + { + "permissionName": "print-events-storage.print-events-status.item.post", + "displayName": "print-events-storage - Fetch print event status", + "description": "Fetch print event details for a batch of request Ids" + }, { "permissionName": "check-in-storage.check-ins.collection.get", "displayName": "Check-in storage - get check-ins collection", @@ -1154,7 +1168,8 @@ "circulation-storage.circulation-settings.item.post", "circulation-storage.circulation-settings.item.put", "circulation-storage.circulation-settings.item.delete", - "print-events-storage.print-events-entry.item.post" + "print-events-storage.print-events-entry.item.post", + "print-events-storage.print-events-status.item.post" ] }, { diff --git a/ramls/examples/print-events-status-request.json b/ramls/examples/print-events-status-request.json new file mode 100644 index 00000000..5d017a3d --- /dev/null +++ b/ramls/examples/print-events-status-request.json @@ -0,0 +1,6 @@ +{ + "requestIds" : [ + "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a", + "fd831be3-f05f-4b6f-b68f-1a976ea1ab0f" + ] +} diff --git a/ramls/examples/print-events-status-response.json b/ramls/examples/print-events-status-response.json new file mode 100644 index 00000000..06236ea3 --- /dev/null +++ b/ramls/examples/print-events-status-response.json @@ -0,0 +1,7 @@ +{ + "requestId": "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a", + "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de16", + "requesterName": "vignesh", + "count": 2, + "printEventDate": "2024-07-04T07:07:00.000+00:00" +} diff --git a/ramls/examples/print-events-status-responses.json b/ramls/examples/print-events-status-responses.json new file mode 100644 index 00000000..2c878bfe --- /dev/null +++ b/ramls/examples/print-events-status-responses.json @@ -0,0 +1,19 @@ +{ + "printEventsStatusResponses": [ + { + "requestId": "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a", + "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de16", + "requesterName": "vignesh", + "count": 2, + "printEventDate": "2024-07-04T07:07:00.000+00:00" + }, + { + "requestId": "fd831be3-f05f-4b6f-b68f-1a976ea1ab0f", + "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de17", + "requesterName": "siddhu", + "count": 5, + "printEventDate": "2024-07-05T07:07:00.000+00:00" + } + ], + "totalRecords": 2 +} diff --git a/ramls/print-events-status-request.json b/ramls/print-events-status-request.json new file mode 100644 index 00000000..010f0185 --- /dev/null +++ b/ramls/print-events-status-request.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Print Events Request", + "type": "object", + "properties": { + "requestIds": { + "description": "List of request IDs", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-5][a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$" + } + } + } +} + diff --git a/ramls/print-events-status-response.json b/ramls/print-events-status-response.json new file mode 100644 index 00000000..e22e9627 --- /dev/null +++ b/ramls/print-events-status-response.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Print events details", + "type": "object", + "properties": { + "requestId": { + "description": "ID of the request", + "type": "string" + }, + "requesterId": { + "description": "ID of the requester", + "type": "string" + }, + "requesterName": { + "description": "Name of the requester", + "type": "string" + }, + "count": { + "description": "No of times the request is printed", + "type": "integer" + }, + "printEventDate": { + "description": "Date and time when the print command is executed", + "type": "string", + "format": "date-time" + } + } +} diff --git a/ramls/print-events-status-responses.json b/ramls/print-events-status-responses.json new file mode 100644 index 00000000..8bddf672 --- /dev/null +++ b/ramls/print-events-status-responses.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of print events details", + "type": "object", + "properties": { + "printEventsStatusResponses": { + "description": "List of print events details", + "id": "printEvents", + "type": "array", + "items": { + "type": "object", + "$ref": "print-events-status-response.json" + } + }, + "totalRecords": { + "type": "integer" + } + }, + "required": [ + "printEventsStatusResponses", + "totalRecords" + ] +} diff --git a/ramls/print-events-storage.raml b/ramls/print-events-storage.raml index a5f25d90..038f86a0 100644 --- a/ramls/print-events-storage.raml +++ b/ramls/print-events-storage.raml @@ -10,6 +10,8 @@ documentation: types: print-events-request: !include print-events-request.json + print-events-status-request: !include print-events-status-request.json + print-events-status-responses: !include print-events-status-responses.json errors: !include raml-util/schemas/errors.schema traits: @@ -46,3 +48,26 @@ traits: body: text/plain: example: "Internal server error" + /print-events-status: + post: + is: [validate] + description: Fetch batch of print event details + body: + application/json: + type: print-events-status-request + responses: + 200: + description: "Requests print event details are successfully retreived" + body: + application/json: + type: print-events-status-responses + 422: + description: "Unprocessable entity" + body: + application/json: + type: errors + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" diff --git a/src/main/java/org/folio/rest/impl/PrintEventsApi.java b/src/main/java/org/folio/rest/impl/PrintEventsApi.java index 78c19932..023de7d0 100644 --- a/src/main/java/org/folio/rest/impl/PrintEventsApi.java +++ b/src/main/java/org/folio/rest/impl/PrintEventsApi.java @@ -4,6 +4,7 @@ import io.vertx.core.Context; import io.vertx.core.Handler; import org.folio.rest.jaxrs.model.PrintEventsRequest; +import org.folio.rest.jaxrs.model.PrintEventsStatusRequest; import org.folio.rest.jaxrs.resource.PrintEventsStorage; import org.folio.service.PrintEventsService; import org.slf4j.Logger; @@ -25,4 +26,12 @@ public void postPrintEventsStoragePrintEventsEntry(PrintEventsRequest printEvent .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) .onFailure(throwable -> asyncResultHandler.handle(succeededFuture(PostPrintEventsStoragePrintEventsEntryResponse.respond500WithTextPlain(throwable.getMessage())))); } + + @Override + public void postPrintEventsStoragePrintEventsStatus(PrintEventsStatusRequest entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOG.info("postPrintEventsStoragePrintEventsStatus:: Fetching print event details for requestIds {}", + entity.getRequestIds()); + new PrintEventsService(vertxContext, okapiHeaders) + .getPrintEventRequestDetails(entity.getRequestIds(), asyncResultHandler); + } } diff --git a/src/main/java/org/folio/service/PrintEventsService.java b/src/main/java/org/folio/service/PrintEventsService.java index 32001289..be083823 100644 --- a/src/main/java/org/folio/service/PrintEventsService.java +++ b/src/main/java/org/folio/service/PrintEventsService.java @@ -1,18 +1,33 @@ package org.folio.service; +import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.rest.RestVerticle; import org.folio.rest.jaxrs.model.PrintEventsRequest; +import org.folio.rest.jaxrs.model.PrintEventsStatusResponse; +import org.folio.rest.jaxrs.model.PrintEventsStatusResponses; import org.folio.rest.jaxrs.resource.PrintEventsStorage; import org.folio.rest.model.PrintEvent; import org.folio.rest.persist.PgUtil; +import org.folio.rest.persist.PostgresClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.persist.PgUtil.postgresClient; +import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; import static org.folio.support.ModuleConstants.PRINT_EVENTS_TABLE; public class PrintEventsService { @@ -22,6 +37,23 @@ public class PrintEventsService { private final Context vertxContext; private final Map okapiHeaders; + private static final String PRINT_EVENT_FETCH_QUERY = """ + WITH cte AS ( + SELECT id, jsonb->>'requestId' AS request_id, jsonb->>'printEventDate' AS last_updated_date, + jsonb->>'requesterName' AS requester_name, jsonb->>'requesterId' AS requester_id, + COUNT(*) OVER (PARTITION BY jsonb->>'requestId') AS request_count, + ROW_NUMBER() OVER (PARTITION BY jsonb->>'requestId' + ORDER BY (jsonb->>'printEventDate')::timestamptz DESC) AS rank + FROM %s.%s + where jsonb->>'requestId' in (%s) + ) + SELECT request_id, requester_name, requester_id, request_count, (last_updated_date)::timestamptz + FROM cte + WHERE + rank = 1; + """; + + public PrintEventsService(Context vertxContext, Map okapiHeaders) { this.vertxContext = vertxContext; @@ -41,4 +73,53 @@ public Future create(PrintEventsRequest printEventRequest) { return PgUtil.postSync(PRINT_EVENTS_TABLE, printEvents, MAX_ENTITIES, false, okapiHeaders, vertxContext, PrintEventsStorage.PostPrintEventsStoragePrintEventsEntryResponse.class); } + + public void getPrintEventRequestDetails(List requestIds, Handler> asyncResultHandler) { + LOG.debug("getPrintEventRequestDetails:: Fetching print event details for requestIds {}", requestIds); + String tenantId = okapiHeaders.get(RestVerticle.OKAPI_HEADER_TENANT); + PostgresClient postgresClient = postgresClient(vertxContext, okapiHeaders); + postgresClient.execute(formatQuery(tenantId, requestIds), handler -> { + try { + if (handler.succeeded()) { + asyncResultHandler.handle( + succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsStatusResponse + .respond200WithApplicationJson(mapRowSetToResponse(handler.result())))); + } else { + LOG.warn("getPrintEventRequestDetails:: Error while executing query", handler.cause()); + asyncResultHandler.handle(succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsStatusResponse + .respond500WithTextPlain(handler.cause()))); + } + } catch (Exception ex) { + LOG.warn("getPrintEventRequestDetails:: Error while fetching print details", ex); + asyncResultHandler.handle(succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsEntryResponse + .respond500WithTextPlain(ex.getMessage()))); + } + }); + } + + private String formatQuery(String tenantId, List requestIds) { + String formattedRequestIds = requestIds + .stream() + .map(requestId -> "'" + requestId + "'") + .collect(Collectors.joining(", ")); + return String.format(PRINT_EVENT_FETCH_QUERY, convertToPsqlStandard(tenantId), PRINT_EVENTS_TABLE, formattedRequestIds); + } + + private PrintEventsStatusResponses mapRowSetToResponse(RowSet rowSet) { + PrintEventsStatusResponses printEventsStatusResponses = new PrintEventsStatusResponses(); + List responseList = new ArrayList<>(); + rowSet.forEach(row -> { + var response = new PrintEventsStatusResponse(); + response.setRequestId(row.getString("request_id")); + response.setRequesterName(row.getString("requester_name")); + response.setRequesterId(row.getString("requester_id")); + response.setCount(row.getInteger("request_count")); + response.setPrintEventDate(Date.from(row.getLocalDateTime("last_updated_date") + .atZone(ZoneOffset.UTC).toInstant())); + responseList.add(response); + }); + printEventsStatusResponses.setPrintEventsStatusResponses(responseList); + printEventsStatusResponses.setTotalRecords(rowSet.size()); + return printEventsStatusResponses; + } } diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 6dab8342..76f591e7 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -529,7 +529,17 @@ { "tableName": "print_events", "withMetadata": true, - "withAuditing": false + "withAuditing": false, + "index": [ + { + "fieldName": "requestId", + "tOps": "ADD", + "caseSensitive": true, + "removeAccents": false, + "sqlExpression": "(jsonb->>'requestId')", + "sqlExpressionQuery": "$" + } + ] } ], "scripts": [ diff --git a/src/test/java/org/folio/rest/api/PrintEventsAPITest.java b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java index 3bd2796a..c7f93248 100644 --- a/src/test/java/org/folio/rest/api/PrintEventsAPITest.java +++ b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java @@ -8,12 +8,16 @@ import java.net.MalformedURLException; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.stream.IntStream; import static org.folio.rest.support.http.InterfaceUrls.printEventsUrl; import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isCreated; +import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isOk; import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isUnprocessableEntity; +import static org.hamcrest.core.Is.is; import static org.hamcrest.junit.MatcherAssert.assertThat; public class PrintEventsAPITest extends ApiTests { @@ -65,12 +69,128 @@ public void createPrintEventLogWhenRequestListIsEmpty() throws MalformedURLExcep assertThat(postResponse, isUnprocessableEntity()); } + @Test + public void createAndGetPrintEventDetails() throws MalformedURLException, ExecutionException, InterruptedException { + List requestIds = IntStream.range(0, 10) + .mapToObj(notUsed -> UUID.randomUUID()) + .toList(); + + // Creating print event entry for batch of requestIds + JsonObject printEventsJson = getPrintEvent(); + printEventsJson.put("requestIds", requestIds); + printEventsJson.put("requesterName", "requester1"); + CompletableFuture postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + JsonResponse postResponse = postCompleted.get(); + assertThat(postResponse, isCreated()); + + // Fetching the print event status details for the batch of requestIds + CompletableFuture printEventStatusResponse = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-status"), createPrintRequestIds(requestIds), StorageTestSuite.TENANT_ID, + ResponseHandler.json(printEventStatusResponse)); + JsonResponse response = printEventStatusResponse.get(); + assertThat(response, isOk()); + var jsonObject = response.getJson(); + assertThat(jsonObject.getInteger("totalRecords"), is(10)); + var printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses"); + IntStream.range(0, printEventsArray.size()) + .mapToObj(printEventsArray::getJsonObject) + .forEach(printEvent -> { + assertThat(printEvent.getInteger("count"), is(1)); + assertThat(printEvent.getString("requesterName"), is("requester1")); + assertThat(printEvent.getString("requesterId"), is("5f5751b4-e352-4121-adca-204b0c2aec43")); + assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:30:00.000+00:00")); + }); + + // creating another print event entry for first 5 requestIds in batch + var requestId2 = UUID.randomUUID(); + printEventsJson = getPrintEvent(); + printEventsJson.put("requestIds", requestIds.subList(0, 5)); + printEventsJson.put("requesterId", requestId2); + printEventsJson.put("requesterName", "requester2"); + printEventsJson.put("printEventDate", "2024-07-15T14:32:00Z"); + postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + postResponse = postCompleted.get(); + assertThat(postResponse, isCreated()); + + // Fetching the print event status details for the first 5 request Ids in batch. + // As the first 5 request ids are printed twice + // count will be 2 and the latest requester id, name and printDate will be returned + printEventStatusResponse = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-status"), + createPrintRequestIds(requestIds.subList(0, 5)), StorageTestSuite.TENANT_ID, + ResponseHandler.json(printEventStatusResponse)); + response = printEventStatusResponse.get(); + assertThat(response, isOk()); + jsonObject = response.getJson(); + assertThat(jsonObject.getInteger("totalRecords"), is(5)); + printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses"); + IntStream.range(0, printEventsArray.size()) + .mapToObj(printEventsArray::getJsonObject) + .forEach(printEvent -> { + assertThat(printEvent.getInteger("count"), is(2)); + assertThat(printEvent.getString("requesterName"), is("requester2")); + assertThat(printEvent.getString("requesterId"), is(requestId2.toString())); + assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:32:00.000+00:00")); + }); + + // Fetching the print event status details for the last 5 request Ids from batch + printEventStatusResponse = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-status"), + createPrintRequestIds(requestIds.subList(5, requestIds.size())), StorageTestSuite.TENANT_ID, + ResponseHandler.json(printEventStatusResponse)); + response = printEventStatusResponse.get(); + assertThat(response, isOk()); + jsonObject = response.getJson(); + assertThat(jsonObject.getInteger("totalRecords"), is(5)); + printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses"); + IntStream.range(0, printEventsArray.size()) + .mapToObj(printEventsArray::getJsonObject) + .forEach(printEvent -> { + assertThat(printEvent.getInteger("count"), is(1)); + assertThat(printEvent.getString("requesterName"), is("requester1")); + assertThat(printEvent.getString("requesterId"), is("5f5751b4-e352-4121-adca-204b0c2aec43")); + assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:30:00.000+00:00")); + }); + } + + @Test + public void getPrintEventStatusWithEmptyRequestIds() throws MalformedURLException, ExecutionException, InterruptedException { + JsonObject printEventsStatusRequestJson = createPrintRequestIds(List.of()); + CompletableFuture getCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-status"), printEventsStatusRequestJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(getCompleted)); + JsonResponse postResponse = getCompleted.get(); + assertThat(postResponse, isUnprocessableEntity()); + } + + @Test + public void getPrintEventStatusWithInvalidRequestIds() throws MalformedURLException, ExecutionException, InterruptedException { + CompletableFuture printEventStatusResponse = new CompletableFuture<>(); + JsonObject printEventsStatusRequestJson = createPrintRequestIds(List.of(UUID.randomUUID(), UUID.randomUUID())); + client.post(printEventsUrl("/print-events-status"), printEventsStatusRequestJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(printEventStatusResponse)); + JsonResponse response = printEventStatusResponse.get(); + assertThat(response, isOk()); + var jsonObject = response.getJson(); + assertThat(jsonObject.getInteger("totalRecords"), is(0)); + assertThat(jsonObject.getJsonArray("printEventsStatusResponses").size(), is(0)); + } + private JsonObject getPrintEvent() { List requestIds = List.of("5f5751b4-e352-4121-adca-204b0c2aec43", "5f5751b4-e352-4121-adca-204b0c2aec44"); return new JsonObject() .put("requestIds", requestIds) .put("requesterId", "5f5751b4-e352-4121-adca-204b0c2aec43") .put("requesterName", "requester") - .put("printEventDate", "2024-06-25T14:30:00Z"); + .put("printEventDate", "2024-07-15T14:30:00Z"); + } + + private JsonObject createPrintRequestIds(List requestIds) { + return new JsonObject() + .put("requestIds", requestIds); } }