diff --git a/NEWS.md b/NEWS.md index c1f7a60c4..4d32578ff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,8 @@ * [MODSOURCE-709](https://issues.folio.org/browse/MODSOURCE-709) MARC authority record is not created when use Job profile with match profile by absent subfield/field * [MODSOURCE-677](https://issues.folio.org/browse/MODSOURCE-677) Import is completed with errors when control field that differs from 001 is used for marc-to-marc matching * [MODSOURCE-722](https://issues.folio.org/browse/MODSOURCE-722) deleteMarcIndexersOldVersions: relation "marc_records_tracking" does not exist +* [MODSOURMAN-1106](https://issues.folio.org/browse/MODSOURMAN-1106) The status of Instance is '-' in the Import log after uploading file. The numbers of updated SRS and Instance are not displayed in the Summary table. +* [MODSOURCE-717](https://issues.folio.org/browse/MODSOURCE-717) MARC modifications not processed when placed after Holdings Update action in a job profile ## 2023-10-13 v5.7.0 * [MODSOURCE-648](https://issues.folio.org/browse/MODSOURCE-648) Upgrade mod-source-record-storage to Java 17 diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b4a41bee3..757f604a6 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -92,6 +92,15 @@ "source-storage.records.put" ] }, + { + "methods": [ + "PUT" + ], + "pathPattern": "/source-storage/records/{id}/generation", + "permissionsRequired": [ + "source-storage.records.put" + ] + }, { "methods": [ "DELETE" diff --git a/mod-source-record-storage-server/src/main/java/org/folio/rest/impl/SourceStorageRecordsImpl.java b/mod-source-record-storage-server/src/main/java/org/folio/rest/impl/SourceStorageRecordsImpl.java index 8469b3d19..70bd30db8 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/rest/impl/SourceStorageRecordsImpl.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/rest/impl/SourceStorageRecordsImpl.java @@ -98,6 +98,21 @@ public void putSourceStorageRecordsById(String id, Record entity, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + vertxContext.runOnContext(v -> { + try { + recordService.updateRecordGeneration(matchedId, entity, tenantId) + .map(updated -> PutSourceStorageRecordsGenerationByIdResponse.respond200WithApplicationJson(entity)) + .map(Response.class::cast).otherwise(ExceptionHelper::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOG.warn("putSourceStorageRecordsGenerationById:: Failed to update record generation by matchedId {}", matchedId, e); + asyncResultHandler.handle(Future.succeededFuture(ExceptionHelper.mapExceptionToResponse(e))); + } + }); + } @Override public void deleteSourceStorageRecordsById(String id, Map okapiHeaders, diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/RecordService.java b/mod-source-record-storage-server/src/main/java/org/folio/services/RecordService.java index 3e2235d18..8b7cf1899 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/RecordService.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/RecordService.java @@ -89,6 +89,16 @@ public interface RecordService { */ Future updateRecord(Record record, String tenantId); + /** + * Updates record generation with given matched id + * + * @param matchedId matched id + * @param record record to update + * @param tenantId tenant id + * @return future with updated Record generation + */ + Future updateRecordGeneration(String matchedId, Record record, String tenantId); + /** * Searches for {@link SourceRecord} by {@link Condition} and ordered by order fields with offset and limit * diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/RecordServiceImpl.java b/mod-source-record-storage-server/src/main/java/org/folio/services/RecordServiceImpl.java index 30888f373..505ffed11 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/RecordServiceImpl.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/RecordServiceImpl.java @@ -73,6 +73,9 @@ public class RecordServiceImpl implements RecordService { private final RecordDao recordDao; private static final String DUPLICATE_CONSTRAINT = "idx_records_matched_id_gen"; private static final String DUPLICATE_RECORD_MSG = "Incoming file may contain duplicates"; + private static final String MATCHED_ID_NOT_EQUAL_TO_999_FIELD = "Matched id (%s) not equal to 999ff$s (%s) field"; + private static final String RECORD_WITH_GIVEN_MATCHED_ID_NOT_FOUND = "Record with given matched id (%s) not found"; + public static final String UPDATE_RECORD_DUPLICATE_EXCEPTION = "Incoming record could be a duplicate, incoming record generation should not be the same as matched record generation and the execution of job should be started after of creating the previous record generation"; public static final char SUBFIELD_S = 's'; public static final char INDICATOR = 'f'; @@ -152,6 +155,25 @@ public Future updateRecord(Record record, String tenantId) { return recordDao.updateRecord(ensureRecordForeignKeys(record), tenantId); } + @Override + public Future updateRecordGeneration(String matchedId, Record record, String tenantId) { + String marcField999s = getFieldFromMarcRecord(record, TAG_999, INDICATOR, INDICATOR, SUBFIELD_S); + if (!matchedId.equals(marcField999s)) { + return Future.failedFuture(new BadRequestException(format(MATCHED_ID_NOT_EQUAL_TO_999_FIELD, matchedId, marcField999s))); + } + record.setId(UUID.randomUUID().toString()); + + return recordDao.getRecordById(matchedId, tenantId) + .map(r -> r.orElseThrow(() -> new NotFoundException(format(RECORD_WITH_GIVEN_MATCHED_ID_NOT_FOUND, matchedId)))) + .compose(v -> saveRecord(record, tenantId)) + .recover(throwable -> { + if (throwable instanceof DuplicateRecordException) { + return Future.failedFuture(new BadRequestException(UPDATE_RECORD_DUPLICATE_EXCEPTION)); + } + return Future.failedFuture(throwable); + }); + } + @Override public Future getSourceRecords(Condition condition, RecordType recordType, Collection> orderFields, int offset, int limit, String tenantId) { diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/AbstractUpdateModifyEventHandler.java b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/AbstractUpdateModifyEventHandler.java index 1835f8e45..f3c8f29be 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/AbstractUpdateModifyEventHandler.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/AbstractUpdateModifyEventHandler.java @@ -123,10 +123,7 @@ public CompletableFuture handle(DataImportEventPayload p } return recordService.saveRecord(changedRecord, payload.getTenant()); }) - .onSuccess(savedRecord -> { - payload.setEventType(getNextEventType()); - future.complete(payload); - }) + .onSuccess(savedRecord -> submitSuccessfulEventType(payload, future, marcMappingOption)) .onFailure(throwable -> { LOG.warn("handle:: Error while MARC record modifying", throwable); future.completeExceptionally(throwable); @@ -138,6 +135,11 @@ public CompletableFuture handle(DataImportEventPayload p return future; } + protected void submitSuccessfulEventType(DataImportEventPayload payload, CompletableFuture future, MappingDetail.MarcMappingOption marcMappingOption) { + payload.setEventType(getUpdateEventType()); + future.complete(payload); + } + @Override public boolean isEligible(DataImportEventPayload payload) { if (payload.getCurrentNode() != null && ACTION_PROFILE == payload.getCurrentNode().getContentType()) { @@ -149,11 +151,11 @@ public boolean isEligible(DataImportEventPayload payload) { protected abstract boolean isHridFillingNeeded(); - protected abstract String getNextEventType(); + protected abstract String getUpdateEventType(); protected abstract EntityType modifiedEntityType(); - private MappingDetail.MarcMappingOption getMarcMappingOption(MappingProfile mappingProfile) { + protected MappingDetail.MarcMappingOption getMarcMappingOption(MappingProfile mappingProfile) { return mappingProfile.getMappingDetails().getMarcMappingOption(); } @@ -209,7 +211,7 @@ private boolean isUpdateOption(MappingDetail.MarcMappingOption marcMappingOption return marcMappingOption == MappingDetail.MarcMappingOption.UPDATE; } - private MappingProfile retrieveMappingProfile(DataImportEventPayload dataImportEventPayload) { + protected MappingProfile retrieveMappingProfile(DataImportEventPayload dataImportEventPayload) { ProfileSnapshotWrapper mappingProfileWrapper = dataImportEventPayload.getCurrentNode().getChildSnapshotWrappers().get(0); return new JsonObject((Map) mappingProfileWrapper.getContent()).mapTo(MappingProfile.class); } diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcAuthorityUpdateModifyEventHandler.java b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcAuthorityUpdateModifyEventHandler.java index 7b63fd5d6..2a2c4b123 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcAuthorityUpdateModifyEventHandler.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcAuthorityUpdateModifyEventHandler.java @@ -41,7 +41,7 @@ public String getPostProcessingInitializationEventType() { } @Override - protected String getNextEventType() { + protected String getUpdateEventType() { return DI_SRS_MARC_AUTHORITY_RECORD_UPDATED.value(); } diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcBibUpdateModifyEventHandler.java b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcBibUpdateModifyEventHandler.java index af8676df4..15f395639 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcBibUpdateModifyEventHandler.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcBibUpdateModifyEventHandler.java @@ -2,11 +2,14 @@ import static java.util.Objects.isNull; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.folio.ActionProfile.Action.MODIFY; +import static org.folio.ActionProfile.Action.UPDATE; import static org.folio.dataimport.util.RestUtil.OKAPI_TENANT_HEADER; import static org.folio.dataimport.util.RestUtil.OKAPI_TOKEN_HEADER; import static org.folio.dataimport.util.RestUtil.OKAPI_URL_HEADER; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_MODIFIED; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_MODIFIED_READY_FOR_POST_PROCESSING; +import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_UPDATED; import static org.folio.rest.jaxrs.model.EntityType.MARC_BIBLIOGRAPHIC; import static org.folio.services.handlers.match.AbstractMarcMatchEventHandler.CENTRAL_TENANT_ID; import static org.folio.services.util.AdditionalFieldsUtil.isSubfieldExist; @@ -14,11 +17,14 @@ import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; + import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.DataImportEventPayload; @@ -79,7 +85,11 @@ protected boolean isHridFillingNeeded() { } @Override - protected String getNextEventType() { + protected String getUpdateEventType() { + return DI_SRS_MARC_BIB_RECORD_UPDATED.value(); + } + + protected String getModifyEventType() { return DI_SRS_MARC_BIB_RECORD_MODIFIED.value(); } @@ -88,6 +98,17 @@ protected EntityType modifiedEntityType() { return MARC_BIBLIOGRAPHIC; } + @Override + protected void submitSuccessfulEventType(DataImportEventPayload payload, CompletableFuture future, MappingDetail.MarcMappingOption marcMappingOption) { + if (marcMappingOption.value().equals(MODIFY.value())) { + payload.setEventType(getModifyEventType()); + } + if (marcMappingOption.value().equals(UPDATE.value())) { + payload.setEventType(getUpdateEventType()); + } + future.complete(payload); + } + @Override protected Future modifyRecord(DataImportEventPayload dataImportEventPayload, MappingProfile mappingProfile, MappingParameters mappingParameters) { diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcHoldingsUpdateModifyEventHandler.java b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcHoldingsUpdateModifyEventHandler.java index 92e3da223..1abe83e39 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcHoldingsUpdateModifyEventHandler.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/actions/MarcHoldingsUpdateModifyEventHandler.java @@ -40,7 +40,7 @@ public String getPostProcessingInitializationEventType() { } @Override - protected String getNextEventType() { + protected String getUpdateEventType() { return DI_SRS_MARC_HOLDINGS_RECORD_UPDATED.value(); } diff --git a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/match/AbstractMarcMatchEventHandler.java b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/match/AbstractMarcMatchEventHandler.java index f586671fa..be969aa32 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/match/AbstractMarcMatchEventHandler.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/services/handlers/match/AbstractMarcMatchEventHandler.java @@ -30,7 +30,6 @@ import org.jooq.Condition; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/mod-source-record-storage-server/src/main/java/org/folio/verticle/consumers/DataImportConsumersVerticle.java b/mod-source-record-storage-server/src/main/java/org/folio/verticle/consumers/DataImportConsumersVerticle.java index 830d417af..f508bb4b8 100644 --- a/mod-source-record-storage-server/src/main/java/org/folio/verticle/consumers/DataImportConsumersVerticle.java +++ b/mod-source-record-storage-server/src/main/java/org/folio/verticle/consumers/DataImportConsumersVerticle.java @@ -5,6 +5,7 @@ import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_HOLDINGS_CREATED_READY_FOR_POST_PROCESSING; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_HOLDING_CREATED; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_HOLDING_MATCHED; +import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_HOLDING_UPDATED; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_INSTANCE_CREATED; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_INSTANCE_CREATED_READY_FOR_POST_PROCESSING; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_INVENTORY_INSTANCE_MATCHED; @@ -42,6 +43,7 @@ public class DataImportConsumersVerticle extends AbstractConsumerVerticle { DI_INVENTORY_AUTHORITY_CREATED_READY_FOR_POST_PROCESSING.value(), DI_INVENTORY_AUTHORITY_UPDATED_READY_FOR_POST_PROCESSING.value(), DI_INVENTORY_HOLDING_CREATED.value(), + DI_INVENTORY_HOLDING_UPDATED.value(), DI_INVENTORY_HOLDING_MATCHED.value(), DI_INVENTORY_HOLDINGS_CREATED_READY_FOR_POST_PROCESSING.value(), DI_INVENTORY_INSTANCE_CREATED.value(), diff --git a/mod-source-record-storage-server/src/test/java/org/folio/rest/impl/RecordApiTest.java b/mod-source-record-storage-server/src/test/java/org/folio/rest/impl/RecordApiTest.java index 1cfa3e26e..3761a3ef9 100644 --- a/mod-source-record-storage-server/src/test/java/org/folio/rest/impl/RecordApiTest.java +++ b/mod-source-record-storage-server/src/test/java/org/folio/rest/impl/RecordApiTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -12,7 +13,6 @@ import java.util.UUID; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.Lists; import io.restassured.RestAssured; import io.restassured.response.Response; import io.vertx.core.json.JsonArray; @@ -50,12 +50,14 @@ public class RecordApiTest extends AbstractRestVerticleTest { private static final String SIXTH_UUID = UUID.randomUUID().toString(); private static final String SEVENTH_UUID = UUID.randomUUID().toString(); private static final String EIGHTH_UUID = UUID.randomUUID().toString(); + private static final String GENERATION = "generation"; private static RawRecord rawMarcRecord; private static ParsedRecord parsedMarcRecord; private static RawRecord rawEdifactRecord; private static ParsedRecord parsedEdifactRecord; + private static ParsedRecord parsedMarcRecordWith999ff$s; static { try { @@ -67,6 +69,13 @@ public class RecordApiTest extends AbstractRestVerticleTest { .withContent(new ObjectMapper().readValue(TestUtil.readFileFromPath(RAW_EDIFACT_RECORD_CONTENT_SAMPLE_PATH), String.class)); parsedEdifactRecord = new ParsedRecord() .withContent(new ObjectMapper().readValue(TestUtil.readFileFromPath(PARSED_EDIFACT_RECORD_CONTENT_SAMPLE_PATH), JsonObject.class).encode()); + parsedMarcRecordWith999ff$s = new ParsedRecord().withId(FIRST_UUID) + .withContent(new JsonObject().put("leader", "01542ccm a2200361 4500") + .put("fields", new JsonArray().add(new JsonObject().put("999", new JsonObject() + .put("subfields", + new JsonArray().add(new JsonObject().put("s", FIRST_UUID))) + .put("ind1", "f") + .put("ind2", "f")))).encode()); } catch (IOException e) { e.printStackTrace(); } @@ -589,6 +598,120 @@ public void shouldUpdateErrorRecordOnPut(TestContext testContext) { async.complete(); } + @Test + public void shouldSendBadRequestWhen999ff$sIsNullDuringUpdateRecordGeneration(TestContext testContext) { + postSnapshots(testContext, snapshot_1); + + Async async = testContext.async(); + Response createResponse = RestAssured.given() + .spec(spec) + .body(record_1) + .when() + .post(SOURCE_STORAGE_RECORDS_PATH); + assertThat(createResponse.statusCode(), is(HttpStatus.SC_CREATED)); + Record createdRecord = createResponse.body().as(Record.class); + + RestAssured.given() + .spec(spec) + .body(createdRecord) + .when() + .put(SOURCE_STORAGE_RECORDS_PATH + "/" + createdRecord.getId() + "/" + GENERATION) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST); + async.complete(); + } + + @Test + public void shouldSendBadRequestWhenMatchedIfNotEqualTo999ff$sDuringUpdateRecordGeneration(TestContext testContext) { + postSnapshots(testContext, snapshot_1); + + Async async = testContext.async(); + Response createResponse = RestAssured.given() + .spec(spec) + .body(record_1.withParsedRecord(parsedMarcRecordWith999ff$s)) + .when() + .post(SOURCE_STORAGE_RECORDS_PATH); + assertThat(createResponse.statusCode(), is(HttpStatus.SC_CREATED)); + Record createdRecord = createResponse.body().as(Record.class); + + RestAssured.given() + .spec(spec) + .body(createdRecord) + .when() + .put(SOURCE_STORAGE_RECORDS_PATH + "/" + UUID.randomUUID() + "/" + GENERATION) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST); + async.complete(); + } + + @Test + public void shouldSendNotFoundWhenUpdateRecordGenerationForNonExistingRecord(TestContext testContext) { + Async async = testContext.async(); + RestAssured.given() + .spec(spec) + .body(record_1.withParsedRecord(parsedMarcRecordWith999ff$s)) + .when() + .put(SOURCE_STORAGE_RECORDS_PATH + "/" + record_1.getMatchedId() + "/" + GENERATION) + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); + async.complete(); + } + + @Test + public void shouldSendBadRequestWhenUpdateRecordGenerationWithDuplicate(TestContext testContext) { + postSnapshots(testContext, snapshot_1); + + Async async = testContext.async(); + Response createResponse = RestAssured.given() + .spec(spec) + .body(record_1.withParsedRecord(parsedMarcRecordWith999ff$s)) + .when() + .post(SOURCE_STORAGE_RECORDS_PATH); + assertThat(createResponse.statusCode(), is(HttpStatus.SC_CREATED)); + Record createdRecord = createResponse.body().as(Record.class); + + postSnapshots(testContext, snapshot_2); + Record recordForUpdate = createdRecord.withSnapshotId(snapshot_2.getJobExecutionId()); + + RestAssured.given() + .spec(spec) + .body(recordForUpdate) + .when() + .put(SOURCE_STORAGE_RECORDS_PATH + "/" + createdRecord.getMatchedId() + "/" + GENERATION) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST); + async.complete(); + } + + @Test + public void shouldUpdateRecordGeneration(TestContext testContext) { + postSnapshots(testContext, snapshot_1); + + Async async = testContext.async(); + Response createResponse = RestAssured.given() + .spec(spec) + .body(record_1.withParsedRecord(parsedMarcRecordWith999ff$s)) + .when() + .post(SOURCE_STORAGE_RECORDS_PATH); + assertThat(createResponse.statusCode(), is(HttpStatus.SC_CREATED)); + Record createdRecord = createResponse.body().as(Record.class); + + postSnapshots(testContext, snapshot_2); + Record recordForUpdate = createdRecord.withSnapshotId(snapshot_2.getJobExecutionId()).withGeneration(null); + + RestAssured.given() + .spec(spec) + .body(recordForUpdate) + .when() + .put(SOURCE_STORAGE_RECORDS_PATH + "/" + createdRecord.getMatchedId() + "/" + GENERATION) + .then() + .statusCode(HttpStatus.SC_OK) + .body("id", not(createdRecord.getId())) + .body("matchedId", is(recordForUpdate.getMatchedId())) + .body("generation", is(1)); + async.complete(); + } + @Test public void shouldReturnNotFoundOnGetByIdWhenRecordDoesNotExist() { RestAssured.given() diff --git a/mod-source-record-storage-server/src/test/java/org/folio/services/MarcBibUpdateModifyEventHandlerTest.java b/mod-source-record-storage-server/src/test/java/org/folio/services/MarcBibUpdateModifyEventHandlerTest.java index 5cad4a3f3..2fa088007 100644 --- a/mod-source-record-storage-server/src/test/java/org/folio/services/MarcBibUpdateModifyEventHandlerTest.java +++ b/mod-source-record-storage-server/src/test/java/org/folio/services/MarcBibUpdateModifyEventHandlerTest.java @@ -12,6 +12,7 @@ import static org.folio.ActionProfile.Action.MODIFY; import static org.folio.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_CREATED; import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_MODIFIED; +import static org.folio.rest.jaxrs.model.DataImportEventTypes.DI_SRS_MARC_BIB_RECORD_UPDATED; import static org.folio.rest.jaxrs.model.EntityType.MARC_BIBLIOGRAPHIC; import static org.folio.rest.jaxrs.model.MappingDetail.MarcMappingOption.UPDATE; import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE; @@ -461,7 +462,7 @@ public void shouldUpdateMatchedMarcRecordWithFieldFromIncomingRecord(TestContext // then future.whenComplete((eventPayload, throwable) -> { context.assertNull(throwable); - context.assertEquals(DI_SRS_MARC_BIB_RECORD_MODIFIED.value(), eventPayload.getEventType()); + context.assertEquals(DI_SRS_MARC_BIB_RECORD_UPDATED.value(), eventPayload.getEventType()); Record actualRecord = Json.decodeValue(dataImportEventPayload.getContext().get(MARC_BIBLIOGRAPHIC.value()), Record.class); @@ -516,7 +517,7 @@ public void shouldUpdateMarcRecordAndCreate035FieldAndRemove003Field(TestContext // then future.whenComplete((eventPayload, throwable) -> { context.assertNull(throwable); - context.assertEquals(DI_SRS_MARC_BIB_RECORD_MODIFIED.value(), eventPayload.getEventType()); + context.assertEquals(DI_SRS_MARC_BIB_RECORD_UPDATED.value(), eventPayload.getEventType()); Record actualRecord = Json.decodeValue(dataImportEventPayload.getContext().get(MARC_BIBLIOGRAPHIC.value()), Record.class); @@ -570,7 +571,7 @@ public void shouldUpdateMarcRecordAndCreate035FieldAndRemove035withDuplicateHrId // then future.whenComplete((eventPayload, throwable) -> { context.assertNull(throwable); - context.assertEquals(DI_SRS_MARC_BIB_RECORD_MODIFIED.value(), eventPayload.getEventType()); + context.assertEquals(DI_SRS_MARC_BIB_RECORD_UPDATED.value(), eventPayload.getEventType()); Record actualRecord = Json.decodeValue(dataImportEventPayload.getContext().get(MARC_BIBLIOGRAPHIC.value()), Record.class); @@ -895,7 +896,7 @@ public void shouldReturnExceptionForDuplicateRecord(TestContext context) { // then future1.whenComplete((eventPayload, throwable) -> { context.assertNull(throwable); - context.assertEquals(DI_SRS_MARC_BIB_RECORD_MODIFIED.value(), eventPayload.getEventType()); + context.assertEquals(DI_SRS_MARC_BIB_RECORD_UPDATED.value(), eventPayload.getEventType()); Record actualRecord = Json.decodeValue(dataImportEventPayloadOriginalRecord.getContext().get(MARC_BIBLIOGRAPHIC.value()), Record.class); @@ -1001,7 +1002,7 @@ private void verifyBibRecordUpdate(String incomingParsedContent, String expected .whenComplete((eventPayload, throwable) -> { var actualRecord = Json.decodeValue(dataImportEventPayload.getContext().get(MARC_BIBLIOGRAPHIC.value()), Record.class); context.assertEquals(Record.State.ACTUAL, actualRecord.getState()); - context.assertEquals(DI_SRS_MARC_BIB_RECORD_MODIFIED.value(), eventPayload.getEventType()); + context.assertEquals(DI_SRS_MARC_BIB_RECORD_UPDATED.value(), eventPayload.getEventType()); context.assertNull(throwable); verifyRecords(context, getParsedContentWithoutDate(expectedParsedContent), diff --git a/mod-source-record-storage-server/src/test/java/org/folio/services/RecordServiceTest.java b/mod-source-record-storage-server/src/test/java/org/folio/services/RecordServiceTest.java index 6b0863e02..a1da2b8e1 100644 --- a/mod-source-record-storage-server/src/test/java/org/folio/services/RecordServiceTest.java +++ b/mod-source-record-storage-server/src/test/java/org/folio/services/RecordServiceTest.java @@ -35,6 +35,7 @@ import org.folio.rest.jaxrs.model.Record.State; import org.folio.rest.jaxrs.model.RecordCollection; import org.folio.rest.jaxrs.model.RecordsBatchResponse; +import org.folio.rest.jaxrs.model.Snapshot; import org.folio.rest.jaxrs.model.SourceRecord; import org.folio.rest.jaxrs.model.SourceRecordCollection; import org.folio.rest.jaxrs.model.StrippedParsedRecord; @@ -49,11 +50,14 @@ import org.junit.Test; import org.junit.runner.RunWith; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotFoundException; import java.io.IOException; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -381,6 +385,226 @@ public void shouldSaveMarcBibRecordWithMatchedIdFrom999field(TestContext context }); } + @Test + public void shouldFailDuringUpdateRecordGenerationIfIncomingMatchedIdNotEqualToMatchedIdFrom999field(TestContext context) { + String matchedId = UUID.randomUUID().toString(); + String marc999 = UUID.randomUUID().toString(); + Record original = TestMocks.getMarcBibRecord(); + ParsedRecord parsedRecord = new ParsedRecord().withId(marc999) + .withContent(new JsonObject().put("leader", "01542ccm a2200361 4500") + .put("fields", new JsonArray().add(new JsonObject().put("999", new JsonObject() + .put("subfields", + new JsonArray().add(new JsonObject().put("s", marc999))) + .put("ind1", "f") + .put("ind2", "f")))).encode()); + Record record = new Record() + .withId(UUID.randomUUID().toString()) + .withSnapshotId(original.getSnapshotId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withOrder(original.getOrder()) + .withRawRecord(original.getRawRecord()) + .withParsedRecord(parsedRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + Async async = context.async(); + + recordService.updateRecordGeneration(matchedId, record, TENANT_ID).onComplete(save -> { + context.assertTrue(save.failed()); + context.assertTrue(save.cause() instanceof BadRequestException); + recordDao.getRecordByMatchedId(matchedId, TENANT_ID).onComplete(get -> { + if (get.failed()) { + context.fail(get.cause()); + } + context.assertTrue(get.result().isEmpty()); + async.complete(); + }); + }); + } + + @Test + public void shouldFailDuringUpdateRecordGenerationIfRecordWithIdAsIncomingMatchedIfNotExist(TestContext context) { + String matchedId = UUID.randomUUID().toString(); + Record original = TestMocks.getMarcBibRecord(); + ParsedRecord parsedRecord = new ParsedRecord().withId(matchedId) + .withContent(new JsonObject().put("leader", "01542ccm a2200361 4500") + .put("fields", new JsonArray().add(new JsonObject().put("999", new JsonObject() + .put("subfields", + new JsonArray().add(new JsonObject().put("s", matchedId))) + .put("ind1", "f") + .put("ind2", "f")))).encode()); + Record record = new Record() + .withId(UUID.randomUUID().toString()) + .withSnapshotId(original.getSnapshotId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withOrder(original.getOrder()) + .withRawRecord(original.getRawRecord()) + .withParsedRecord(parsedRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + Async async = context.async(); + + recordService.updateRecordGeneration(matchedId, record, TENANT_ID).onComplete(save -> { + context.assertTrue(save.failed()); + context.assertTrue(save.cause() instanceof NotFoundException); + recordDao.getRecordByMatchedId(matchedId, TENANT_ID).onComplete(get -> { + if (get.failed()) { + context.fail(get.cause()); + } + context.assertTrue(get.result().isEmpty()); + async.complete(); + }); + }); + } + + @Test + public void shouldFailUpdateRecordGenerationIfDuplicateError(TestContext context) { + String matchedId = UUID.randomUUID().toString(); + Record original = TestMocks.getMarcBibRecord(); + + Record record1 = new Record() + .withId(matchedId) + .withSnapshotId(original.getSnapshotId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withOrder(original.getOrder()) + .withRawRecord(rawRecord) + .withParsedRecord(marcRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + + Snapshot snapshot = new Snapshot().withJobExecutionId(UUID.randomUUID().toString()) + .withProcessingStartedDate(new Date()) + .withStatus(Snapshot.Status.PROCESSING_IN_PROGRESS); + + ParsedRecord parsedRecord = new ParsedRecord().withId(matchedId) + .withContent(new JsonObject().put("leader", "01542ccm a2200361 4500") + .put("fields", new JsonArray().add(new JsonObject().put("999", new JsonObject() + .put("subfields", + new JsonArray().add(new JsonObject().put("s", matchedId))) + .put("ind1", "f") + .put("ind2", "f")))).encode()); + Record recordToUpdateGeneration = new Record() + .withId(UUID.randomUUID().toString()) + .withSnapshotId(snapshot.getJobExecutionId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withGeneration(0) + .withOrder(original.getOrder()) + .withRawRecord(original.getRawRecord()) + .withParsedRecord(parsedRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + Async async = context.async(); + + recordService.saveRecord(record1, TENANT_ID).onComplete(record1Saved -> { + if (record1Saved.failed()) { + context.fail(record1Saved.cause()); + } + context.assertNotNull(record1Saved.result().getRawRecord()); + context.assertNotNull(record1Saved.result().getParsedRecord()); + context.assertEquals(record1Saved.result().getState(), State.ACTUAL); + compareRecords(context, record1, record1Saved.result()); + + SnapshotDaoUtil.save(postgresClientFactory.getQueryExecutor(TENANT_ID), snapshot).onComplete(snapshotSaved -> { + if (snapshotSaved.failed()) { + context.fail(snapshotSaved.cause()); + } + recordService.updateRecordGeneration(matchedId, recordToUpdateGeneration, TENANT_ID).onComplete(recordToUpdateGenerationSaved -> { + context.assertTrue(recordToUpdateGenerationSaved.failed()); + context.assertTrue(recordToUpdateGenerationSaved.cause() instanceof BadRequestException); + async.complete(); + }); + }); + }); + } + + @Test + public void shouldUpdateRecordGeneration(TestContext context) { + String matchedId = UUID.randomUUID().toString(); + Record original = TestMocks.getMarcBibRecord(); + + Record record1 = new Record() + .withId(matchedId) + .withSnapshotId(original.getSnapshotId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withOrder(original.getOrder()) + .withRawRecord(rawRecord) + .withParsedRecord(marcRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + + Snapshot snapshot = new Snapshot().withJobExecutionId(UUID.randomUUID().toString()) + .withProcessingStartedDate(new Date()) + .withStatus(Snapshot.Status.PROCESSING_IN_PROGRESS); + + ParsedRecord parsedRecord = new ParsedRecord().withId(matchedId) + .withContent(new JsonObject().put("leader", "01542ccm a2200361 4500") + .put("fields", new JsonArray().add(new JsonObject().put("999", new JsonObject() + .put("subfields", + new JsonArray().add(new JsonObject().put("s", matchedId))) + .put("ind1", "f") + .put("ind2", "f")))).encode()); + Record recordToUpdateGeneration = new Record() + .withId(UUID.randomUUID().toString()) + .withSnapshotId(snapshot.getJobExecutionId()) + .withRecordType(original.getRecordType()) + .withState(State.ACTUAL) + .withOrder(original.getOrder()) + .withRawRecord(original.getRawRecord()) + .withParsedRecord(parsedRecord) + .withAdditionalInfo(original.getAdditionalInfo()) + .withExternalIdsHolder(new ExternalIdsHolder().withInstanceId(UUID.randomUUID().toString())) + .withMetadata(original.getMetadata()); + Async async = context.async(); + + recordService.saveRecord(record1, TENANT_ID).onComplete(record1Saved -> { + if (record1Saved.failed()) { + context.fail(record1Saved.cause()); + } + context.assertNotNull(record1Saved.result().getRawRecord()); + context.assertNotNull(record1Saved.result().getParsedRecord()); + context.assertEquals(record1Saved.result().getState(), State.ACTUAL); + compareRecords(context, record1, record1Saved.result()); + + SnapshotDaoUtil.save(postgresClientFactory.getQueryExecutor(TENANT_ID), snapshot).onComplete(snapshotSaved -> { + if (snapshotSaved.failed()) { + context.fail(snapshotSaved.cause()); + } + recordService.updateRecordGeneration(matchedId, recordToUpdateGeneration, TENANT_ID).onComplete(recordToUpdateGenerationSaved -> { + context.assertTrue(recordToUpdateGenerationSaved.succeeded()); + context.assertEquals(recordToUpdateGenerationSaved.result().getMatchedId(), matchedId); + context.assertEquals(recordToUpdateGenerationSaved.result().getGeneration(), 1); + recordDao.getRecordByMatchedId(matchedId, TENANT_ID).onComplete(get -> { + if (get.failed()) { + context.fail(get.cause()); + } + context.assertTrue(get.result().isPresent()); + context.assertEquals(get.result().get().getGeneration(), 1); + context.assertEquals(get.result().get().getMatchedId(), matchedId); + context.assertNotEquals(get.result().get().getId(), matchedId); + context.assertEquals(get.result().get().getState(), State.ACTUAL); + recordDao.getRecordById(matchedId, TENANT_ID).onComplete(getRecord1 -> { + if (getRecord1.failed()) { + context.fail(get.cause()); + } + context.assertTrue(getRecord1.result().isPresent()); + context.assertEquals(getRecord1.result().get().getState(), State.OLD); + async.complete(); + }); + }); + }); + }); + }); + } + @Test public void shouldSaveMarcBibRecordWithMatchedIdFromRecordId(TestContext context) { Record original = TestMocks.getMarcBibRecord(); diff --git a/ramls/source-record-storage-records.raml b/ramls/source-record-storage-records.raml index 78d10b78a..de6517f41 100644 --- a/ramls/source-record-storage-records.raml +++ b/ramls/source-record-storage-records.raml @@ -154,4 +154,19 @@ resourceTypes: body: text/plain: example: "Internal server error" - + /generation: + displayName: Generation + is: [validate] + put: + description: Updates a specific Record with incremented generation and state ACTUAL by matched id + body: + application/json: + type: record + example: + strict: false + value: !include raml-storage/examples/mod-source-record-storage/record.sample + responses: + 200: + body: + application/json: + type: record