Skip to content

Commit 48fc39c

Browse files
committed
MODSOURCE-859: Create new API POST /source-storage/records/{id}/un-delete
1 parent 59b04d5 commit 48fc39c

File tree

7 files changed

+107
-0
lines changed

7 files changed

+107
-0
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
* [MODSOURCE-860](https://folio-org.atlassian.net/browse/MODSOURCE-860) "Numerics only" option of existing record section does not work during MARC-BIB to MARC-BIB matching
1111
* [MODINV-1114](https://folio-org.atlassian.net/browse/MODINV-1114) Extend matching records endpoint to support multiple marc-bib match results processing
1212
* [MODSOURCE-863](https://folio-org.atlassian.net/browse/MODSOURCE-863) Add index to speed up the querying of a composite record
13+
* [MODSOURCE-859](https://folio-org.atlassian.net/browse/MODSOURCE-859) Added record undelete endpoint
14+
15+
| METHOD | URL | DESCRIPTION |
16+
|--------|------------------------------------------|-----------------------------------------------------|
17+
| POST | /source-storage/records/{id}/un-delete | Undelete the record by setting the state to ACTUAL. |
1318

1419
## 2024-10-28 5.9.0
1520
* [MODSOURCE-767](https://folio-org.atlassian.net/browse/MODSOURCE-767) Single record overlay creates duplicate OCLC#/035

descriptors/ModuleDescriptor-template.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@
101101
"source-storage.records.generation.item.put"
102102
]
103103
},
104+
{
105+
"methods": [
106+
"POST"
107+
],
108+
"pathPattern": "/source-storage/records/{id}/un-delete",
109+
"permissionsRequired": [
110+
"source-storage.records.undelete.item.post"
111+
]
112+
},
104113
{
105114
"methods": [
106115
"DELETE"
@@ -356,6 +365,11 @@
356365
"displayName": "Source Storage - update record's generation",
357366
"description": "Update record's generation"
358367
},
368+
{
369+
"permissionName": "source-storage.records.undelete.item.post",
370+
"displayName": "Source Storage - undelete record",
371+
"description": "Undelete record"
372+
},
359373
{
360374
"permissionName": "source-storage.parsed-records.collection.put",
361375
"displayName": "Source Storage - update records",
@@ -474,6 +488,7 @@
474488
"source-storage.migrations.post",
475489
"source-storage.migrations.item.get",
476490
"source-storage.records.generation.item.put",
491+
"source-storage.records.undelete.item.post",
477492
"source-storage.parsed-records.collection.put",
478493
"source-storage.batch-records.collection.post",
479494
"source-storage.batch.records.collection.post",

mod-source-record-storage-server/src/main/java/org/folio/rest/impl/SourceStorageRecordsImpl.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,18 @@ public void postSourceStorageRecordsMatching(RecordMatchingDto recordMatchingDto
193193
});
194194
}
195195

196+
@Override
197+
public void postSourceStorageRecordsUnDeleteById(String id, String idType, Map<String, String> okapiHeaders,
198+
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
199+
vertxContext.runOnContext(v -> {
200+
try {
201+
recordService.unDeleteRecordById(id, toExternalIdType(idType), okapiHeaders).map(r -> true)
202+
.map(updated -> PostSourceStorageRecordsUnDeleteByIdResponse.respond204()).map(Response.class::cast)
203+
.otherwise(ExceptionHelper::mapExceptionToResponse).onComplete(asyncResultHandler);
204+
} catch (Exception e) {
205+
LOG.warn("postSourceStorageRecordsUnDeleteById:: Failed to undelete record by id {}", id, e);
206+
asyncResultHandler.handle(Future.succeededFuture(ExceptionHelper.mapExceptionToResponse(e)));
207+
}
208+
});
209+
}
196210
}

mod-source-record-storage-server/src/main/java/org/folio/services/RecordService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,6 @@ Future<RecordsBatchResponse> saveRecordsByExternalIds(List<String> externalIds,
283283
Future<Void> updateRecordsState(String matchedId, RecordState state, RecordType recordType, String tenantId);
284284

285285
Future<Void> deleteRecordById(String id, IdType idType, Map<String, String> okapiHeaders);
286+
287+
Future<Void> unDeleteRecordById(String id, IdType idType, Map<String, String> okapiHeaders);
286288
}

mod-source-record-storage-server/src/main/java/org/folio/services/RecordServiceImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public class RecordServiceImpl implements RecordService {
9797
private static final String RECORD_WITH_GIVEN_MATCHED_ID_NOT_FOUND = "Record with given matched id (%s) not found";
9898
private static final String NOT_FOUND_MESSAGE = "%s with id '%s' was not found";
9999
private static final Character DELETED_LEADER_RECORD_STATUS = 'd';
100+
private static final Character CORRECTED_LEADER_RECORD_STATUS = 'c';
100101
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";
101102
public static final String EXTERNAL_IDS_MISSING_ERROR = "MARC_BIB records must contain external instance and hr id's and 001 field into parsed record";
102103
protected static final String UPDATE_RECORD_WITH_LINKED_DATA_ID_EXCEPTION = "Record with source=LINKED_DATA cannot be updated using QuickMARC. Please use Linked Data Editor.";
@@ -421,6 +422,21 @@ public Future<Void> deleteRecordById(String id, IdType idType, Map<String, Strin
421422
.compose(record -> updateRecord(record, okapiHeaders)).map(r -> null);
422423
}
423424

425+
@Override
426+
public Future<Void> unDeleteRecordById(String id, IdType idType, Map<String, String> okapiHeaders) {
427+
var tenantId = okapiHeaders.get(OKAPI_TENANT_HEADER);
428+
return recordDao.getRecordByExternalId(id, idType, tenantId)
429+
.map(recordOptional -> recordOptional.orElseThrow(() -> new NotFoundException(format(NOT_FOUND_MESSAGE, Record.class.getSimpleName(), id))))
430+
.map(record -> {
431+
update005field(record);
432+
record.withState(Record.State.ACTUAL);
433+
record.setAdditionalInfo(record.getAdditionalInfo().withDeleted(false));
434+
ParsedRecordDaoUtil.updateLeaderStatus(record.getParsedRecord(), CORRECTED_LEADER_RECORD_STATUS);
435+
return record;
436+
})
437+
.compose(record -> updateRecord(record, okapiHeaders)).map(r -> null);
438+
}
439+
424440
private Future<Record> setMatchedIdForRecord(Record record, String tenantId) {
425441
String marcField999s = getFieldFromMarcRecord(record, TAG_999, INDICATOR, INDICATOR, SUBFIELD_S);
426442
if (marcField999s != null) {

mod-source-record-storage-server/src/test/java/org/folio/services/RecordServiceTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,49 @@ public void shouldHardDeleteMarcRecord(TestContext context) {
17141714
});
17151715
}
17161716

1717+
@Test
1718+
public void shouldUnDeleteMarcRecord(TestContext context) {
1719+
Async async = context.async();
1720+
var marcBibMock = TestMocks.getMarcBibRecord();
1721+
var sourceRecord = new Record()
1722+
.withId(UUID.randomUUID().toString())
1723+
.withSnapshotId(marcBibMock.getSnapshotId())
1724+
.withRecordType(marcBibMock.getRecordType())
1725+
.withState(State.DELETED)
1726+
.withOrder(marcBibMock.getOrder())
1727+
.withRawRecord(rawRecord)
1728+
.withParsedRecord(marcRecord)
1729+
.withAdditionalInfo(marcBibMock.getAdditionalInfo())
1730+
.withExternalIdsHolder(
1731+
new ExternalIdsHolder()
1732+
.withInstanceId(UUID.randomUUID().toString())
1733+
.withInstanceHrid(RandomStringUtils.randomAlphanumeric(9)))
1734+
.withMetadata(marcBibMock.getMetadata());
1735+
1736+
var okapiHeaders = Map.of(OKAPI_TENANT_HEADER, TENANT_ID);
1737+
1738+
recordService.saveRecord(sourceRecord, okapiHeaders).onComplete(saveResult -> {
1739+
if (saveResult.failed()) {
1740+
context.fail(saveResult.cause());
1741+
}
1742+
recordService.unDeleteRecordById(sourceRecord.getId(), IdType.RECORD, okapiHeaders).onComplete(undeleteResult -> {
1743+
if (undeleteResult.failed()) {
1744+
context.fail(undeleteResult.cause());
1745+
}
1746+
recordService.getRecordById(sourceRecord.getId(), TENANT_ID).onComplete(getResult -> {
1747+
if (getResult.failed()) {
1748+
context.fail(getResult.cause());
1749+
}
1750+
context.assertTrue(getResult.result().isPresent());
1751+
context.assertFalse(getResult.result().get().getDeleted());
1752+
verify(recordDomainEventPublisher, times(1))
1753+
.publishRecordUpdated(eq(saveResult.result()), eq(getResult.result().get()), any());
1754+
});
1755+
async.complete();
1756+
});
1757+
});
1758+
}
1759+
17171760
@Test
17181761
public void shouldSoftDeleteMarcRecord(TestContext context) {
17191762
Async async = context.async();

ramls/source-record-storage-records.raml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,15 @@ resourceTypes:
197197
body:
198198
application/json:
199199
type: record
200+
/un-delete:
201+
displayName: Undelete
202+
post:
203+
description: Undelete specific record
204+
queryParameters:
205+
idType:
206+
description: Type of Id for Record lookup
207+
type: string
208+
example: INSTANCE
209+
default: RECORD
210+
responses:
211+
204:

0 commit comments

Comments
 (0)