From 2aa41b41f111e675499f73ee8edac506b9be6976 Mon Sep 17 00:00:00 2001 From: SreejaMangarapu <164345887+SreejaMangarapu@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:53:58 +0530 Subject: [PATCH 1/5] CIRCSTORE-512 added test case for EnableRequestPrintDetailsSetting (#470) --- .../rest/api/CirculationSettingsAPITest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java index 5ec12cba..4f896fac 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -72,4 +72,24 @@ private JsonObject getCirculationSetting(String id) { .put("value", new JsonObject().put("sample", "OK")); } + @Test + public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + String id = UUID.randomUUID().toString(); + JsonObject enableRequestPrintDetailsSettingJson = new JsonObject(); + enableRequestPrintDetailsSettingJson.put("id", id); + enableRequestPrintDetailsSettingJson.put("name", "Enable Request Print"); + enableRequestPrintDetailsSettingJson.put("value", new JsonObject().put("Enable Request Print", true)); + + JsonObject circulationSettingsResponse = + circulationSettingsClient.create(enableRequestPrintDetailsSettingJson).getJson(); + JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); + + assertThat(circulationSettingsResponse.getString("id"), is(id)); + assertThat(circulationSettingsResponse.getString("name"), + is(enableRequestPrintDetailsSettingJson.getString("name"))); + assertThat(circulationSettingsById.getString("id"), is(id)); + assertThat(circulationSettingsById.getJsonObject("value"), + is(enableRequestPrintDetailsSettingJson.getJsonObject("value"))); + } } From 65cef7f35ef222e92f785c914347e7c8d762af82 Mon Sep 17 00:00:00 2001 From: julianladisch Date: Fri, 12 Jul 2024 19:15:14 +0200 Subject: [PATCH 2/5] CIRCSTORE-496: Keep existing reference/sample data on upgrade (#468) * CIRCSTORE-496: Keep existing reference/sample data on upgrade https://folio-org.atlassian.net/browse/CIRCSTORE-496 * CIRCSTORE-496 Add logging * CIRCSTORE-496 Test * CIRCSTORE-496 Revert test --------- Co-authored-by: alexanderkurash --- .../org/folio/rest/impl/TenantRefAPI.java | 97 ++++++++++++------- .../org/folio/rest/api/TenantRefApiTests.java | 32 ++++++ 2 files changed, 96 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/folio/rest/impl/TenantRefAPI.java b/src/main/java/org/folio/rest/impl/TenantRefAPI.java index bca5f474..ff145ee2 100644 --- a/src/main/java/org/folio/rest/impl/TenantRefAPI.java +++ b/src/main/java/org/folio/rest/impl/TenantRefAPI.java @@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.support.kafka.topic.CirculationStorageKafkaTopic; +import org.folio.dbschema.Versioned; import org.folio.kafka.services.KafkaAdminClientService; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.TenantAttributes; @@ -20,8 +21,6 @@ import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; public class TenantRefAPI extends TenantAPI { @@ -41,37 +40,69 @@ Future loadData(TenantAttributes attributes, String tenantId, .compose(r -> new KafkaAdminClientService(vertxContext.owner()) .createKafkaTopics(CirculationStorageKafkaTopic.values(), tenantId)) .compose(r -> super.loadData(attributes, tenantId, headers, vertxContext)) - .compose(superRecordsLoaded -> { - log.info("Initializing of tenant's data"); - Vertx vertx = vertxContext.owner(); - Promise promise = Promise.promise(); - TenantLoading tl = new TenantLoading(); - tl.withKey(REFERENCE_KEY).withLead(REFERENCE_LEAD) - .withIdContent() - .add("loan-policy-storage/loan-policies") - .add("request-policy-storage/request-policies") - .add("patron-notice-policy-storage/patron-notice-policies") - .add("staff-slips-storage/staff-slips") - .withIdRaw() - .add("circulation-rules-storage") - .withIdContent() - .add("cancellation-reason-storage/cancellation-reasons") - .withKey(SAMPLE_KEY).withLead(SAMPLE_LEAD) - .add("loans", "loan-storage/loans") - .add("requests", "request-storage/requests") - .add("circulation-settings-storage/circulation-settings"); - - - tl.perform(attributes, headers, vertx, res -> { - if (res.failed()) { - promise.fail(res.cause()); - } else { - PubSubRegistrationService.registerModule(headers, vertx) - .whenComplete((aBoolean, throwable) -> promise.complete()); - } - }); - return promise.future(); - }); + .compose(r -> loadData(attributes, headers, vertxContext)) + .compose(r -> registerModuleInPubSub(headers, vertxContext)) + .mapEmpty(); + } + + private Future loadData(TenantAttributes attributes, Map headers, + Context vertxContext) { + + log.info("loadData:: Initializing tenant data"); + + TenantLoading tenantLoading = new TenantLoading(); + tenantLoading.withKey(REFERENCE_KEY).withLead(REFERENCE_LEAD); + if (isNew(attributes, "14.0.0")) { + log.info("loadData:: Adding reference data: loan policies, request policies, " + + "patron notice policies, staff slips, circulation rules, cancellation reasons"); + + tenantLoading.withIdContent() + .add("loan-policy-storage/loan-policies") + .add("request-policy-storage/request-policies") + .add("patron-notice-policy-storage/patron-notice-policies") + .add("staff-slips-storage/staff-slips") + .withIdRaw() + .add("circulation-rules-storage") + .withIdContent() + .add("cancellation-reason-storage/cancellation-reasons"); + } + tenantLoading.withKey(SAMPLE_KEY).withLead(SAMPLE_LEAD); + if (isNew(attributes, "7.0.0")) { + log.info("loadData:: Adding sample data: loans, circulation settings"); + + tenantLoading.add("loans", "loan-storage/loans") + .add("circulation-settings-storage/circulation-settings"); + } + if (isNew(attributes, "16.1.0")) { + log.info("loadData:: Adding sample data: requests"); + + tenantLoading.add("requests", "request-storage/requests"); + } + return tenantLoading.perform(attributes, headers, vertxContext, 0); + } + + /** + * Returns attributes.getModuleFrom() < featureVersion or + * attributes.getModuleFrom() is null. + */ + static boolean isNew(TenantAttributes attributes, String featureVersion) { + log.info("isNew:: params moduleFrom: {}, moduleTo: {}, purge: {}, featureVersion: {}", + attributes::getModuleFrom, attributes::getModuleTo, attributes::getPurge, + () -> featureVersion); + if (attributes.getModuleFrom() == null) { + log.info("isNew:: moduleFrom is null, quitting"); + return true; + } + var since = new Versioned() { }; + since.setFromModuleVersion(featureVersion); + var result = since.isNewForThisInstall(attributes.getModuleFrom()); + log.info("isNew:: {}", result); + return result; + } + + private Future registerModuleInPubSub(Map headers, Context vertxContext) { + var vertx = vertxContext.owner(); + return Future.fromCompletionStage(PubSubRegistrationService.registerModule(headers, vertx)); } @Validate diff --git a/src/test/java/org/folio/rest/api/TenantRefApiTests.java b/src/test/java/org/folio/rest/api/TenantRefApiTests.java index e3a40745..2c39c7f8 100644 --- a/src/test/java/org/folio/rest/api/TenantRefApiTests.java +++ b/src/test/java/org/folio/rest/api/TenantRefApiTests.java @@ -19,6 +19,8 @@ import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_FILLED; import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_PICKUP_EXPIRED; import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_UNFILLED; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -43,6 +45,7 @@ import org.folio.rest.jaxrs.model.TenantAttributes; import org.folio.rest.jaxrs.model.TenantJob; import org.folio.rest.persist.PostgresClient; +import org.folio.rest.tools.utils.ModuleName; import org.folio.rest.tools.utils.NetworkUtils; import org.junit.AfterClass; import org.junit.Before; @@ -66,6 +69,7 @@ import io.vertx.ext.web.client.WebClient; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; @RunWith(VertxUnitRunner.class) public class TenantRefApiTests { @@ -108,6 +112,7 @@ public class TenantRefApiTests { private static final String REQUEST_ID_MISSING_SUFFIX = "ecd86aab-a0ac-4d3c-bb90-0df390b1c6c4"; private static final String REQUEST_ID_MISSING_PICKUP_SERVICE_POINT_NAME = "87a7dfd9-8fdb-4b0d-9529-14912b484860"; + private static final String OTHER_CANCELLATION_REASON_ID = "b548b182-55c2-4741-b169-616d9cd995a8"; private static StubMapping itemStorageStub; private static StubMapping holdingsStorageStub; @@ -456,6 +461,17 @@ public void migrationShouldNotCorrectFulfillmentPreferenceSpellingFromMigratedVe }); } + @Test + public void keepReferenceData(TestContext context) { + setOtherCancellationReasonName("foo") + .compose(x -> assertOtherCancellationReasonName(context, "foo")) + .compose(x -> postTenant(context, "16.1.0", ModuleName.getModuleVersion())) + .compose(x -> assertOtherCancellationReasonName(context, "foo")) + .compose(x -> postTenant(context, "0.0.0", ModuleName.getModuleVersion())) + .compose(x -> assertOtherCancellationReasonName(context, "Other")) + .onComplete(context.asyncAssertSuccess()); + } + private void jobFailsWhenRequestValidationFails(TestContext context, Async async, JsonObject request, String expectedErrorMessage) { @@ -595,6 +611,22 @@ private void validateRequestSearchMigrationResult(TestContext context, JsonObjec } } + private static Future> setOtherCancellationReasonName(String name) { + var json = new JsonObject() + .put("id", OTHER_CANCELLATION_REASON_ID) + .put("name", name) + .put("description", "Other") + .put("requiresAdditionalInformation", true); + return postgresClient.execute("UPDATE cancellation_reason SET jsonb=$1 WHERE id=$2", + Tuple.of(json, OTHER_CANCELLATION_REASON_ID)); + } + + private static Future assertOtherCancellationReasonName(TestContext context, String expected) { + return postgresClient.selectSingle("SELECT jsonb->>'name' FROM cancellation_reason WHERE id=$1", + Tuple.of(OTHER_CANCELLATION_REASON_ID)) + .onComplete(context.asyncAssertSuccess(row -> assertThat(row.getString(0), is(expected)))); + } + static void deleteTenant(TenantClient tenantClient) { CompletableFuture future = new CompletableFuture<>(); tenantClient.deleteTenantByOperationId(jobId, deleted -> { From 2ade1907c61630ebcc0aeb72a56667c9c0b31ec7 Mon Sep 17 00:00:00 2001 From: SreejaMangarapu <164345887+SreejaMangarapu@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:41:57 +0530 Subject: [PATCH 3/5] Circstore-513 Implementing post print Event API (#469) * CIRCSTORE-513 implementing post print Event Api including test cases --- descriptors/ModuleDescriptor-template.json | 23 +++++- ramls/examples/print-events-request.json | 9 +++ ramls/print-events-request.json | 38 ++++++++++ ramls/print-events-storage.raml | 48 ++++++++++++ .../org/folio/rest/impl/PrintEventsApi.java | 28 +++++++ .../java/org/folio/rest/model/PrintEvent.java | 15 ++++ .../org/folio/service/PrintEventsService.java | 44 +++++++++++ .../org/folio/support/ModuleConstants.java | 1 + .../templates/db_scripts/schema.json | 5 ++ .../folio/rest/api/PrintEventsAPITest.java | 76 +++++++++++++++++++ .../org/folio/rest/api/StorageTestSuite.java | 3 +- .../rest/support/http/InterfaceUrls.java | 3 + 12 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 ramls/examples/print-events-request.json create mode 100644 ramls/print-events-request.json create mode 100644 ramls/print-events-storage.raml create mode 100644 src/main/java/org/folio/rest/impl/PrintEventsApi.java create mode 100644 src/main/java/org/folio/rest/model/PrintEvent.java create mode 100644 src/main/java/org/folio/service/PrintEventsService.java create mode 100644 src/test/java/org/folio/rest/api/PrintEventsAPITest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index a5069c69..f14ed302 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -645,6 +645,21 @@ } ] }, + { + "id": "print-events-storage", + "version": "1.0", + "handlers": [ + { + "methods": [ + "POST" + ], + "pathPattern": "/print-events-storage/print-events-entry", + "permissionsRequired": [ + "print-events-storage.print-events-entry.item.post" + ] + } + ] + }, { "id": "_timer", "version": "1.0", @@ -677,6 +692,11 @@ } ], "permissionSets": [ + { + "permissionName": "print-events-storage.print-events-entry.item.post", + "displayName": "print events storage - save print event logs", + "description": "save print event log in storage" + }, { "permissionName": "check-in-storage.check-ins.collection.get", "displayName": "Check-in storage - get check-ins collection", @@ -1133,7 +1153,8 @@ "circulation-storage.circulation-settings.item.get", "circulation-storage.circulation-settings.item.post", "circulation-storage.circulation-settings.item.put", - "circulation-storage.circulation-settings.item.delete" + "circulation-storage.circulation-settings.item.delete", + "print-events-storage.print-events-entry.item.post" ] }, { diff --git a/ramls/examples/print-events-request.json b/ramls/examples/print-events-request.json new file mode 100644 index 00000000..8505827a --- /dev/null +++ b/ramls/examples/print-events-request.json @@ -0,0 +1,9 @@ +{ + "requestIds": [ + "059e54bb-53e5-4039-a2fb-b34358e88b0a", + "e70dcbae-30c6-47ac-94f8-4ffefd44a935" + ], + "requesterId": "d51470ea-5daa-480b-a4aa-09c8c6d9940e", + "requesterName": "requester", + "printEventDate": "2024-06-25T20:00:00+05:30" +} diff --git a/ramls/print-events-request.json b/ramls/print-events-request.json new file mode 100644 index 00000000..3f169b1c --- /dev/null +++ b/ramls/print-events-request.json @@ -0,0 +1,38 @@ +{ + "$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}$" + } + }, + "requesterId": { + "description": "ID of the requester", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema", + "pattern": "^(?!\\s*$).+" + }, + "requesterName": { + "description": "Name of the requester", + "type": "string", + "pattern": "^(?!\\s*$).+" + }, + "printEventDate": { + "description": "Date and time when the print command is executed", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "requestIds", + "requesterId", + "requesterName", + "printEventDate" + ] +} diff --git a/ramls/print-events-storage.raml b/ramls/print-events-storage.raml new file mode 100644 index 00000000..a5f25d90 --- /dev/null +++ b/ramls/print-events-storage.raml @@ -0,0 +1,48 @@ +#%RAML 1.0 +title: Print Events Storage +version: v1.0 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: Print Events Storage API + content: Storage for print events + +types: + print-events-request: !include print-events-request.json + errors: !include raml-util/schemas/errors.schema + +traits: + validate: !include raml-util/traits/validation.raml + +/print-events-storage: + /print-events-entry: + post: + is: [validate] + description: save a print event log + body: + application/json: + type: print-events-request + responses: + 201: + description: "All items have been successfully created or updated" + 409: + description: "Optimistic locking version conflict" + body: + text/plain: + example: "Version error" + 413: + description: "Payload too large" + body: + text/plain: + example: "Payload too large" + 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 new file mode 100644 index 00000000..78c19932 --- /dev/null +++ b/src/main/java/org/folio/rest/impl/PrintEventsApi.java @@ -0,0 +1,28 @@ +package org.folio.rest.impl; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import org.folio.rest.jaxrs.model.PrintEventsRequest; +import org.folio.rest.jaxrs.resource.PrintEventsStorage; +import org.folio.service.PrintEventsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.Response; +import java.util.Map; + +import static io.vertx.core.Future.succeededFuture; + +public class PrintEventsApi implements PrintEventsStorage { + private static final Logger LOG = LoggerFactory.getLogger(PrintEventsApi.class); + + @Override + public void postPrintEventsStoragePrintEventsEntry(PrintEventsRequest printEventsRequest, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOG.info("postPrintEventsStoragePrintEvents:: save print events {}", printEventsRequest); + new PrintEventsService(vertxContext, okapiHeaders) + .create(printEventsRequest) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(throwable -> asyncResultHandler.handle(succeededFuture(PostPrintEventsStoragePrintEventsEntryResponse.respond500WithTextPlain(throwable.getMessage())))); + } +} diff --git a/src/main/java/org/folio/rest/model/PrintEvent.java b/src/main/java/org/folio/rest/model/PrintEvent.java new file mode 100644 index 00000000..82cc49b9 --- /dev/null +++ b/src/main/java/org/folio/rest/model/PrintEvent.java @@ -0,0 +1,15 @@ +package org.folio.rest.model; + +import lombok.Data; +import org.folio.rest.jaxrs.model.Metadata; +import java.util.Date; + +@Data +public class PrintEvent { + private String id; + private String requestId; + private String requesterId; + private String requesterName; + private Date printEventDate; + private Metadata metadata; +} diff --git a/src/main/java/org/folio/service/PrintEventsService.java b/src/main/java/org/folio/service/PrintEventsService.java new file mode 100644 index 00000000..32001289 --- /dev/null +++ b/src/main/java/org/folio/service/PrintEventsService.java @@ -0,0 +1,44 @@ +package org.folio.service; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import org.folio.rest.jaxrs.model.PrintEventsRequest; +import org.folio.rest.jaxrs.resource.PrintEventsStorage; +import org.folio.rest.model.PrintEvent; +import org.folio.rest.persist.PgUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; + +import static org.folio.support.ModuleConstants.PRINT_EVENTS_TABLE; + +public class PrintEventsService { + + private static final Logger LOG = LoggerFactory.getLogger(PrintEventsService.class); + private static final int MAX_ENTITIES = 10000; + private final Context vertxContext; + private final Map okapiHeaders; + + + public PrintEventsService(Context vertxContext, Map okapiHeaders) { + this.vertxContext = vertxContext; + this.okapiHeaders = okapiHeaders; + } + + public Future create(PrintEventsRequest printEventRequest) { + LOG.info("create:: save print events {}", printEventRequest); + List printEvents = printEventRequest.getRequestIds().stream().map(requestId -> { + PrintEvent event = new PrintEvent(); + event.setRequestId(requestId); + event.setRequesterId(printEventRequest.getRequesterId()); + event.setRequesterName(printEventRequest.getRequesterName()); + event.setPrintEventDate(printEventRequest.getPrintEventDate()); + return event; + }).toList(); + return PgUtil.postSync(PRINT_EVENTS_TABLE, printEvents, MAX_ENTITIES, false, okapiHeaders, vertxContext, + PrintEventsStorage.PostPrintEventsStoragePrintEventsEntryResponse.class); + } +} diff --git a/src/main/java/org/folio/support/ModuleConstants.java b/src/main/java/org/folio/support/ModuleConstants.java index ba92fd1e..f3ef2f52 100644 --- a/src/main/java/org/folio/support/ModuleConstants.java +++ b/src/main/java/org/folio/support/ModuleConstants.java @@ -29,6 +29,7 @@ public class ModuleConstants { TlrFeatureToggleJob.class; public static final String REQUEST_POLICY_TABLE = "request_policy"; public static final Class REQUEST_POLICY_CLASS = RequestPolicy.class; + public static final String PRINT_EVENTS_TABLE = "print_events"; private ModuleConstants(){ } diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 5ad43489..6dab8342 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -525,6 +525,11 @@ "removeAccents": true } ] + }, + { + "tableName": "print_events", + "withMetadata": true, + "withAuditing": false } ], "scripts": [ diff --git a/src/test/java/org/folio/rest/api/PrintEventsAPITest.java b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java new file mode 100644 index 00000000..3bd2796a --- /dev/null +++ b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java @@ -0,0 +1,76 @@ +package org.folio.rest.api; + +import io.vertx.core.json.JsonObject; +import org.folio.rest.support.ApiTests; +import org.folio.rest.support.JsonResponse; +import org.folio.rest.support.ResponseHandler; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +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.isUnprocessableEntity; +import static org.hamcrest.junit.MatcherAssert.assertThat; + +public class PrintEventsAPITest extends ApiTests { + + @Test + public void canCreatePrintEventLog() throws MalformedURLException, ExecutionException, InterruptedException { + JsonObject printEventsJson = getPrintEvent(); + final CompletableFuture postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + final JsonResponse postResponse = postCompleted.get(); + assertThat(postResponse, isCreated()); + } + + @Test + public void createPrintEventLogWithMissingFields() throws MalformedURLException, ExecutionException, InterruptedException { + List requestIds = List.of("5f5751b4-e352-4121-adca-204b0c2aec43", "5f5751b4-e352-4121-adca-204b0c2aec44"); + JsonObject printEventsJson = new JsonObject() + .put("requestIds", requestIds) + .put("requesterName", "Sample Requester") + .put("printEventDate", "2024-06-25T14:30:00Z"); + final CompletableFuture postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + final JsonResponse postResponse = postCompleted.get(); + assertThat(postResponse, isUnprocessableEntity()); + } + + @Test + public void createPrintEventLogWithBlankFields() throws MalformedURLException, ExecutionException, InterruptedException { + JsonObject printEventsJson = getPrintEvent(); + printEventsJson.put("requesterId", " "); + final CompletableFuture postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + final JsonResponse postResponse = postCompleted.get(); + assertThat(postResponse, isUnprocessableEntity()); + } + + @Test + public void createPrintEventLogWhenRequestListIsEmpty() throws MalformedURLException, ExecutionException, InterruptedException { + List requestIds = List.of(); + JsonObject printEventsJson = getPrintEvent(); + printEventsJson.put("requestIds", requestIds); + final CompletableFuture postCompleted = new CompletableFuture<>(); + client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID, + ResponseHandler.json(postCompleted)); + final JsonResponse postResponse = postCompleted.get(); + assertThat(postResponse, isUnprocessableEntity()); + } + + 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"); + } +} diff --git a/src/test/java/org/folio/rest/api/StorageTestSuite.java b/src/test/java/org/folio/rest/api/StorageTestSuite.java index bf9dbb70..bc1d76cd 100644 --- a/src/test/java/org/folio/rest/api/StorageTestSuite.java +++ b/src/test/java/org/folio/rest/api/StorageTestSuite.java @@ -82,7 +82,8 @@ ActualCostRecordAPITest.class, EventConsumerVerticleTest.class, CheckOutLockAPITest.class, - CirculationSettingsAPITest.class + CirculationSettingsAPITest.class, + PrintEventsAPITest.class }) public class StorageTestSuite { diff --git a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java index a60caef5..5d05430f 100644 --- a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java +++ b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java @@ -47,6 +47,9 @@ public static URL requestExpirationUrl() throws MalformedURLException { public static URL checkOutStorageUrl(String subPath) throws MalformedURLException { return storageUrl("/check-out-lock-storage" + subPath); } + public static URL printEventsUrl(String subPath) throws MalformedURLException { + return storageUrl("/print-events-storage" + subPath); + } public static URL circulationSettingsUrl(String subPath) throws MalformedURLException { return storageUrl("/circulation-settings-storage/circulation-settings" + subPath); 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 4/5] 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); } } From 2775310d444527fa04d33d48dab7d07f1db2bfa6 Mon Sep 17 00:00:00 2001 From: Antony Hruschev Date: Mon, 19 Aug 2024 19:01:56 +0400 Subject: [PATCH 5/5] CIRCSTORE- 519 Update instead save with the same name circ settings. (#480) * CIRCSTORE-519 merge to master PR * CIRCSTORE-519 stack trace to exception logging --- .../org/folio/persist/AbstractRepository.java | 6 +- .../rest/impl/CirculationSettingsAPI.java | 10 +- .../service/CirculationSettingsService.java | 54 ++++++- .../templates/db_scripts/schema.json | 9 +- .../rest/api/CirculationSettingsAPITest.java | 146 ++++++++++++------ 5 files changed, 169 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/folio/persist/AbstractRepository.java b/src/main/java/org/folio/persist/AbstractRepository.java index 93bdbc7f..02a26236 100644 --- a/src/main/java/org/folio/persist/AbstractRepository.java +++ b/src/main/java/org/folio/persist/AbstractRepository.java @@ -38,6 +38,10 @@ protected AbstractRepository(PostgresClient postgresClient, String tableName, this.recordType = recordType; } + public Future saveAndReturnUpdatedEntity(String id, T entity) { + return postgresClient.saveAndReturnUpdatedEntity(tableName, id, entity); + } + public Future save(String id, T entity) { return postgresClient.save(tableName, id, entity); } @@ -119,4 +123,4 @@ public Future> deleteById(String id) { return postgresClient.delete(tableName, id); } -} +} \ No newline at end of file diff --git a/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java index 07e05a95..a70809f2 100644 --- a/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java +++ b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java @@ -1,5 +1,10 @@ package org.folio.rest.impl; +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.headersFor201; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.respond201WithApplicationJson; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.respond500WithTextPlain; + import java.util.Map; import javax.ws.rs.core.Response; @@ -21,7 +26,10 @@ public void postCirculationSettingsStorageCirculationSettings(String lang, new CirculationSettingsService(vertxContext, okapiHeaders) .create(circulationSettings) - .onComplete(asyncResultHandler); + .onSuccess(response -> asyncResultHandler.handle( + succeededFuture(respond201WithApplicationJson(circulationSettings, headersFor201())))) + .onFailure(throwable -> asyncResultHandler.handle( + succeededFuture(respond500WithTextPlain(throwable.getMessage())))); } @Override diff --git a/src/main/java/org/folio/service/CirculationSettingsService.java b/src/main/java/org/folio/service/CirculationSettingsService.java index a0c95007..c68285fa 100644 --- a/src/main/java/org/folio/service/CirculationSettingsService.java +++ b/src/main/java/org/folio/service/CirculationSettingsService.java @@ -1,26 +1,31 @@ package org.folio.service; +import static org.folio.rest.tools.utils.ValidationHelper.isDuplicate; import static org.folio.service.event.EntityChangedEventPublisherFactory.circulationSettingsEventPublisher; import static org.folio.support.ModuleConstants.CIRCULATION_SETTINGS_TABLE; +import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; +import lombok.extern.log4j.Log4j2; import org.folio.persist.CirculationSettingsRepository; import org.folio.rest.jaxrs.model.CirculationSetting; import org.folio.rest.jaxrs.model.CirculationSettings; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsResponse; -import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PutCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; +import org.folio.rest.persist.Criteria.Criteria; +import org.folio.rest.persist.Criteria.Criterion; import org.folio.rest.persist.PgUtil; import org.folio.service.event.EntityChangedEventPublisher; import io.vertx.core.Context; import io.vertx.core.Future; +@Log4j2 public class CirculationSettingsService { private final Context vertxContext; @@ -42,10 +47,11 @@ public Future getAll(int offset, int limit, String query) { GetCirculationSettingsStorageCirculationSettingsResponse.class); } - public Future create(CirculationSetting circulationSetting) { - return PgUtil.post(CIRCULATION_SETTINGS_TABLE, circulationSetting, okapiHeaders, vertxContext, - PostCirculationSettingsStorageCirculationSettingsResponse.class) - .compose(eventPublisher.publishCreated()); + public Future create(CirculationSetting circulationSetting) { + log.debug("create:: trying to save circulationSetting: {}", circulationSetting); + return repository.saveAndReturnUpdatedEntity(circulationSetting.getId(), + circulationSetting) + .recover(throwable -> updateSettingsValue(circulationSetting, throwable)); } public Future findById(String circulationSettingsId) { @@ -67,4 +73,40 @@ public Future delete(String circulationSettingsId) { .compose(eventPublisher.publishRemoved(circulationSetting)) ); } -} + + private Future updateSettingsValue(CirculationSetting circulationSetting, + Throwable throwable) { + + if (!isDuplicate(throwable.getMessage())) { + log.warn("updateSettingsValue:: error during saving circulation setting: {}", + circulationSetting, throwable); + return Future.failedFuture(throwable); + } + + log.info("updateSettingsValue:: setting with name: {} already exists.", + circulationSetting.getName()); + + return getSettingsByName(circulationSetting.getName()) + .compose(settings -> updateSettings(settings, circulationSetting)); + } + + private Future updateSettings(List settings, + CirculationSetting circulationSetting) { + + settings.forEach(setting -> setting.setValue(circulationSetting.getValue())); + log.debug("updateSettings:: updating {} setting(s) with name '{}'", + settings::size, circulationSetting::getName); + return repository.update(settings) + .map(circulationSetting); + } + + private Future> getSettingsByName(String settingsName) { + log.debug("getSettingsByName:: trying to fetch setting by name: {}", settingsName); + Criterion filter = new Criterion(new Criteria() + .addField("'name'") + .setOperation("=") + .setVal(settingsName)); + + return repository.get(filter); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 76f591e7..f9602bc8 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -154,7 +154,14 @@ { "tableName": "circulation_settings", "withMetadata": true, - "withAuditing": false + "withAuditing": false, + "uniqueIndex": [ + { + "fieldName": "name", + "tOps": "ADD", + "caseSensitive": false + } + ] }, { "tableName": "request", diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java index 4f896fac..08c92861 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -1,95 +1,147 @@ package org.folio.rest.api; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.junit.MatcherAssert.assertThat; - -import java.net.MalformedURLException; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - +import io.vertx.core.json.JsonObject; +import lombok.SneakyThrows; import org.folio.rest.support.ApiTests; import org.folio.rest.support.http.AssertingRecordClient; import org.folio.rest.support.http.InterfaceUrls; +import org.junit.Before; import org.junit.Test; -import io.vertx.core.json.JsonObject; +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.junit.MatcherAssert.assertThat; public class CirculationSettingsAPITest extends ApiTests { + private static final String ID_KEY = "id"; + private static final String NAME_KEY = "name"; + private static final String VALUE_KEY = "value"; + private static final String SAMPLE_VALUE = "sample"; + private static final String SAMPLE_KEY = "sample"; + private static final String INITIAL_VALUE = "OK"; + private static final String UPDATED_VALUE = "OK1"; + private static final String TABLE_NAME = "circulation_settings"; + private static final String CIRCULATION_SETTINGS_PROPERTY = "circulation-settings"; + private static final int NOT_FOUND_STATUS = 404; + private static final String REQUEST_PRINT_SETTING = "Enable Request Print"; private final AssertingRecordClient circulationSettingsClient = new AssertingRecordClient( - client, StorageTestSuite.TENANT_ID, InterfaceUrls::circulationSettingsUrl, - "circulation-settings"); + client, StorageTestSuite.TENANT_ID, InterfaceUrls::circulationSettingsUrl, + CIRCULATION_SETTINGS_PROPERTY); - @Test - public void canCreateAndRetrieveCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @Before + public void beforeEach() { + StorageTestSuite.cleanUpTable(TABLE_NAME); + } + @Test + @SneakyThrows + public void updateInsteadCreateWithTheSameName() { String id = UUID.randomUUID().toString(); JsonObject circulationSettingsJson = getCirculationSetting(id); JsonObject circulationSettingsResponse = circulationSettingsClient.create(circulationSettingsJson).getJson(); + JsonObject circulationSettingsJsonUpdated = getUpdatedSettingsJson(); + circulationSettingsClient.create(circulationSettingsJsonUpdated); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); - assertThat(circulationSettingsResponse.getString("id"), is(id)); - assertThat(circulationSettingsById.getString("id"), is(id)); - assertThat(circulationSettingsById.getJsonObject("value"), is( - circulationSettingsJson.getJsonObject("value"))); + assertThatCorrectCreation(circulationSettingsResponse, circulationSettingsJson); + assertThat(circulationSettingsClient.getAll().getTotalRecords(), is(1)); + assertThat(getValue(circulationSettingsJsonUpdated), is(getValue(circulationSettingsById))); } @Test - public void canUpdateCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @SneakyThrows + public void canCreateAndRetrieveCirculationSettings() { + String id = UUID.randomUUID().toString(); + JsonObject circulationSettingsJson = getCirculationSetting(id); + JsonObject circulationSettingsResponse = + circulationSettingsClient.create(circulationSettingsJson).getJson(); + JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); + assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getJsonObject(VALUE_KEY), is( + circulationSettingsJson.getJsonObject(VALUE_KEY))); + } + + @Test + @SneakyThrows + public void canUpdateCirculationSettings() { String id = UUID.randomUUID().toString(); JsonObject circulationSettingsJson = getCirculationSetting(id); circulationSettingsClient.create(circulationSettingsJson).getJson(); circulationSettingsClient.attemptPutById( - circulationSettingsJson.put("value", new JsonObject().put("sample", "DONE"))); + circulationSettingsJson.put(VALUE_KEY, new JsonObject().put(SAMPLE_KEY, "DONE"))); JsonObject updatedCirculationSettings = circulationSettingsClient.getById(id).getJson(); - assertThat(updatedCirculationSettings.getString("id"), is(id)); - assertThat(updatedCirculationSettings.getJsonObject("value"), is( - circulationSettingsJson.getJsonObject("value"))); + assertThat(updatedCirculationSettings.getString(ID_KEY), is(id)); + assertThat(updatedCirculationSettings.getJsonObject(VALUE_KEY), is( + circulationSettingsJson.getJsonObject(VALUE_KEY))); } @Test - public void canDeleteCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { - + @SneakyThrows + public void canDeleteCirculationSettings() { UUID id = UUID.randomUUID(); circulationSettingsClient.create(getCirculationSetting(id.toString())).getJson(); circulationSettingsClient.deleteById(id); var deletedCirculationSettings = circulationSettingsClient.attemptGetById(id); - assertThat(deletedCirculationSettings.getStatusCode(), is(404)); - } - - private JsonObject getCirculationSetting(String id) { - return new JsonObject() - .put("id", id) - .put("name", "sample") - .put("value", new JsonObject().put("sample", "OK")); + assertThat(deletedCirculationSettings.getStatusCode(), is(NOT_FOUND_STATUS)); } @Test - public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @SneakyThrows + public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() { String id = UUID.randomUUID().toString(); JsonObject enableRequestPrintDetailsSettingJson = new JsonObject(); - enableRequestPrintDetailsSettingJson.put("id", id); - enableRequestPrintDetailsSettingJson.put("name", "Enable Request Print"); - enableRequestPrintDetailsSettingJson.put("value", new JsonObject().put("Enable Request Print", true)); + enableRequestPrintDetailsSettingJson.put(ID_KEY, id); + enableRequestPrintDetailsSettingJson.put(NAME_KEY, REQUEST_PRINT_SETTING); + JsonObject enablePrintSettingJson = new JsonObject().put(REQUEST_PRINT_SETTING, true); + enableRequestPrintDetailsSettingJson.put(VALUE_KEY, enablePrintSettingJson); JsonObject circulationSettingsResponse = circulationSettingsClient.create(enableRequestPrintDetailsSettingJson).getJson(); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); - assertThat(circulationSettingsResponse.getString("id"), is(id)); - assertThat(circulationSettingsResponse.getString("name"), - is(enableRequestPrintDetailsSettingJson.getString("name"))); - assertThat(circulationSettingsById.getString("id"), is(id)); - assertThat(circulationSettingsById.getJsonObject("value"), - is(enableRequestPrintDetailsSettingJson.getJsonObject("value"))); + assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); + assertThat(circulationSettingsResponse.getString(NAME_KEY), + is(enableRequestPrintDetailsSettingJson.getString(NAME_KEY))); + assertThat(circulationSettingsById.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getJsonObject(VALUE_KEY), + is(enableRequestPrintDetailsSettingJson.getJsonObject(VALUE_KEY))); + } + + private static String getValue(JsonObject circulationSettingsById) { + return circulationSettingsById.getJsonObject(VALUE_KEY).getString(SAMPLE_KEY); + } + + private JsonObject getCirculationSetting(String id) { + return new JsonObject() + .put(ID_KEY, id) + .put(NAME_KEY, SAMPLE_VALUE) + .put(VALUE_KEY, new JsonObject().put(SAMPLE_KEY, INITIAL_VALUE)); + } + + private static void assertThatCorrectCreation(JsonObject circulationSettingsResponse, + JsonObject circulationSettingsJson) { + + String actualCreatedId = circulationSettingsResponse.getString(ID_KEY); + String expectedCreatedId = circulationSettingsJson.getString(ID_KEY); + String actualCreatedName = circulationSettingsResponse.getString(NAME_KEY); + String expectedCreatedName = circulationSettingsJson.getString(NAME_KEY); + + assertThat(actualCreatedId, is(expectedCreatedId)); + assertThat(actualCreatedName, is(expectedCreatedName)); + } + + private JsonObject getUpdatedSettingsJson() { + String updatedId = UUID.randomUUID().toString(); + JsonObject circulationSettingsJsonUpdated = getCirculationSetting(updatedId); + JsonObject updatedValue = new JsonObject().put(SAMPLE_KEY, UPDATED_VALUE); + circulationSettingsJsonUpdated.put(VALUE_KEY, updatedValue); + return circulationSettingsJsonUpdated; } -} +} \ No newline at end of file