Skip to content

Commit

Permalink
MODINVSTOR-1223 Implement a POST requestі to get holdings and instances
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytrokrutii authored Jun 5, 2024
1 parent b32b45e commit d2afdc9
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 23 deletions.
5 changes: 3 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
* Description ([ISSUE\_NUMBER](https://folio-org.atlassian.net/browse/ISSUE_NUMBER))

### New APIs versions
* Provides `API_NAME vX.Y`
* Requires `API_NAME vX.Y`
* Provides `instance-storage 10.1`
* Requires `holdings-storage 6.1`

### Features
* Implement domain event production for location create/update/delete ([MODINVSTOR-1181](https://issues.folio.org/browse/MODINVSTOR-1181))
* Implement a POST request to get Holdings and Instances ([MODINVSTOR-1223](https://folio-org.atlassian.net/browse/MODINVSTOR-1223))

### Bug fixes
* Unintended update of instance records \_version (optimistic locking) whenever any of its holdings or items are created, updated or deleted. ([MODINVSTOR-1186](https://folio-org.atlassian.net/browse/MODINVSTOR-1186))
Expand Down
15 changes: 12 additions & 3 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,16 @@
},
{
"id": "holdings-storage",
"version": "6.0",
"version": "6.1",
"handlers": [
{
"methods": ["GET"],
"pathPattern": "/holdings-storage/holdings",
"permissionsRequired": ["inventory-storage.holdings.collection.get"]
},{
"methods": ["POST"],
"pathPattern": "/holdings-storage/holdings/retrieve",
"permissionsRequired": ["inventory-storage.holdings.collection.get"]
}, {
"methods": ["GET"],
"pathPattern": "/holdings-storage/holdings/{id}",
Expand Down Expand Up @@ -142,13 +146,18 @@
},
{
"id": "instance-storage",
"version": "10.0",
"version": "10.1",
"handlers": [
{
"methods": ["GET"],
"pathPattern": "/instance-storage/instances",
"permissionsRequired": ["inventory-storage.instances.collection.get"]
}, {
},{
"methods": ["POST"],
"pathPattern": "/instance-storage/instances/retrieve",
"permissionsRequired": ["inventory-storage.instances.collection.get"]
},
{
"methods": ["GET"],
"pathPattern": "/instance-storage/instances/{id}",
"permissionsRequired": ["inventory-storage.instances.item.get"]
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<ramlfiles_path>${basedir}/ramls/</ramlfiles_path>
<generate_routing_context>/instance-storage/instances,/holdings-storage/holdings,/item-storage/items,/record-bulk/ids,/oai-pmh-view/instances,/oai-pmh-view/updatedInstanceIds,/oai-pmh-view/enrichedInstances,/inventory-hierarchy/updated-instance-ids,/inventory-hierarchy/items-and-holdings,/inventory-view/instances</generate_routing_context>
<generate_routing_context>/instance-storage/instances,/instance-storage/instances/retrieve,/holdings-storage/holdings,/holdings-storage/holdings/retrieve,/item-storage/items,/record-bulk/ids,/oai-pmh-view/instances,/oai-pmh-view/updatedInstanceIds,/oai-pmh-view/enrichedInstances,/inventory-hierarchy/updated-instance-ids,/inventory-hierarchy/items-and-holdings,/inventory-view/instances</generate_routing_context>
<argLine />

<raml-module-builder-version>35.2.2</raml-module-builder-version> <!-- also update vertx.version -->
Expand Down
5 changes: 5 additions & 0 deletions ramls/examples/retrieveEntitiesDto.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"limit": 10,
"offset": 10,
"query": "status=\"Available\""
}
14 changes: 13 additions & 1 deletion ramls/holdings-storage.raml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ types:
holdingsRecords: !include holdings-storage/holdingsRecords.json
holdingsRecordView: !include holdings-storage/holdingsRecordView.json
holdingsRecordViews: !include holdings-storage/holdingsRecordViews.json
retrieveDto: !include retrieveEntitiesDto.json
errors: !include raml-util/schemas/errors.schema

traits:
Expand Down Expand Up @@ -81,4 +82,15 @@ resourceTypes:
type: holdingsRecord
example:
strict: false
value: !include examples/holdings-storage/holdingsRecord_get.json
value: !include examples/holdings-storage/holdingsRecord_get.json
/retrieve:
post:
is: [ validate ]
body:
application/json:
type: retrieveDto
example:
strict: false
value: !include examples/retrieveEntitiesDto.json
description: |
Get Holdings by POST request
14 changes: 14 additions & 0 deletions ramls/instance-storage.raml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ types:
marcJson: !include marc.json
instanceRelationship: !include instancerelationship.json
instanceRelationships: !include instancerelationships.json
retrieveDto: !include retrieveEntitiesDto.json
errors: !include raml-util/schemas/errors.schema

traits:
pageable: !include raml-util/traits/pageable.raml
searchable: !include raml-util/traits/searchable.raml
validate: !include raml-util/traits/validation.raml

resourceTypes:
collection: !include raml-util/rtypes/collection.raml
Expand Down Expand Up @@ -144,3 +147,14 @@ resourceTypes:
body:
text/plain:
example: "Not implemented yet"
/retrieve:
post:
is: [ validate ]
body:
application/json:
type: retrieveDto
example:
strict: false
value: !include examples/retrieveEntitiesDto.json
description: |
Get Instances by POST request
25 changes: 25 additions & 0 deletions ramls/retrieveEntitiesDto.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "DTO for fetching records by POST request",
"type": "object",
"properties": {
"offset": {
"description": "Skip over a number of elements by specifying an offset value for the query",
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"default": 0
},
"limit": {
"description": "Limit the number of elements returned in the response",
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"default": 10
},
"query": {
"description": "A query expressed as a CQL string",
"type": "string"
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/folio/rest/impl/HoldingsStorageApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.folio.rest.annotations.Validate;
import org.folio.rest.jaxrs.model.HoldingsRecord;
import org.folio.rest.jaxrs.model.HoldingsRecordView;
import org.folio.rest.jaxrs.model.RetrieveDto;
import org.folio.rest.jaxrs.resource.HoldingsStorage;
import org.folio.rest.persist.PgUtil;
import org.folio.rest.support.EndpointFailureHandler;
Expand Down Expand Up @@ -85,6 +86,17 @@ public void deleteHoldingsStorageHoldingsByHoldingsRecordId(
.onComplete(asyncResultHandler);
}

@Validate
@Override
public void postHoldingsStorageHoldingsRetrieve(RetrieveDto entity,
RoutingContext routingContext,
Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext) {
PgUtil.streamGet(HOLDINGS_RECORD_TABLE, HoldingsRecordView.class, entity.getQuery(), entity.getOffset(),
entity.getLimit(), null, "holdingsRecords", routingContext, okapiHeaders, vertxContext);
}

@Validate
@Override
public void putHoldingsStorageHoldingsByHoldingsRecordId(
Expand Down
53 changes: 37 additions & 16 deletions src/main/java/org/folio/rest/impl/InstanceStorageApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.folio.rest.jaxrs.model.InstanceRelationships;
import org.folio.rest.jaxrs.model.Instances;
import org.folio.rest.jaxrs.model.MarcJson;
import org.folio.rest.jaxrs.model.RetrieveDto;
import org.folio.rest.jaxrs.resource.InstanceStorage;
import org.folio.rest.persist.Criteria.Limit;
import org.folio.rest.persist.Criteria.Offset;
Expand All @@ -40,6 +41,7 @@

public class InstanceStorageApi implements InstanceStorage {
private static final Logger log = LogManager.getLogger();
private static final String TITLE = "title";
private final Messages messages = Messages.getInstance();

@Validate
Expand Down Expand Up @@ -200,22 +202,7 @@ public void getInstanceStorageInstances(String totalRecords, int offset, int lim
Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext) {

if (PgUtil.checkOptimizedCQL(query, "title") != null) { // Until RMB-573 is fixed
try {
PreparedCql preparedCql = handleCql(query, limit, offset);
PgUtil.getWithOptimizedSql(preparedCql.getTableName(), Instance.class, Instances.class,
"title", query, offset, limit,
okapiHeaders, vertxContext, GetInstanceStorageInstancesResponse.class, asyncResultHandler);
} catch (Exception e) {
log.error(e.getMessage(), e);
asyncResultHandler.handle(io.vertx.core.Future.succeededFuture(
GetInstanceStorageInstancesResponse.respond500WithTextPlain(e.getMessage())));
}
return;
}

PgUtil.streamGet(INSTANCE_TABLE, Instance.class, query, offset, limit, null,
"instances", routingContext, okapiHeaders, vertxContext);
fetchInstances(query, limit, offset, routingContext, okapiHeaders, asyncResultHandler, vertxContext);
}

@Validate
Expand Down Expand Up @@ -387,6 +374,40 @@ public void putInstanceStorageInstancesSourceRecordModsByInstanceId(
.respond500WithTextPlain("Not implemented yet.")));
}

@Validate
@Override
public void postInstanceStorageInstancesRetrieve(RetrieveDto entity,
RoutingContext routingContext,
Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext) {
fetchInstances(entity.getQuery(), entity.getLimit(), entity.getOffset(),
routingContext, okapiHeaders, asyncResultHandler, vertxContext);
}

private void fetchInstances(String query, int limit, int offset,
RoutingContext routingContext,
Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext) {
if (PgUtil.checkOptimizedCQL(query, TITLE) != null) {
try {
PreparedCql preparedCql = handleCql(query, limit, offset);
PgUtil.getWithOptimizedSql(preparedCql.getTableName(), Instance.class, Instances.class,
TITLE, query, offset, limit,
okapiHeaders, vertxContext, GetInstanceStorageInstancesResponse.class, asyncResultHandler);
} catch (Exception e) {
log.error(e.getMessage(), e);
asyncResultHandler.handle(io.vertx.core.Future.succeededFuture(
GetInstanceStorageInstancesResponse.respond500WithTextPlain(e.getMessage())));
}
return;
}

PgUtil.streamGet(INSTANCE_TABLE, Instance.class, query, offset, limit, null,
"instances", routingContext, okapiHeaders, vertxContext);
}

PreparedCql handleCql(String query, int limit, int offset) throws FieldException {
return new PreparedCql(INSTANCE_TABLE, query, limit, offset);
}
Expand Down
41 changes: 41 additions & 0 deletions src/test/java/org/folio/rest/api/HoldingsStorageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,47 @@ public void canGetAllHoldings() {
assertThat(allHoldings.stream().anyMatch(filterById(thirdHoldingId)), is(true));
}

@SneakyThrows
@Test
public void canRetrieveAllHoldings() {
var firstInstanceId = UUID.randomUUID();
var secondInstanceId = UUID.randomUUID();
var thirdInstanceId = UUID.randomUUID();

instancesClient.create(smallAngryPlanet(firstInstanceId));
instancesClient.create(nod(secondInstanceId));
instancesClient.create(uprooted(thirdInstanceId));

CompletableFuture<Response> getCompleted = new CompletableFuture<>();

final var firstHoldingId = holdingsClient.create(new HoldingRequestBuilder()
.forInstance(firstInstanceId)
.withPermanentLocation(MAIN_LIBRARY_LOCATION_ID)).getId();

final var secondHoldingId = holdingsClient.create(new HoldingRequestBuilder()
.forInstance(secondInstanceId)
.withPermanentLocation(ANNEX_LIBRARY_LOCATION_ID)).getId();

final var thirdHoldingId = holdingsClient.create(new HoldingRequestBuilder()
.forInstance(thirdInstanceId)
.withPermanentLocation(MAIN_LIBRARY_LOCATION_ID)
.withTags(new JsonObject().put("tagList", new JsonArray().add(TAG_VALUE)))).getId();

getClient().post(holdingsStorageUrl("/retrieve"), new JsonObject(), TENANT_ID,
ResponseHandler.json(getCompleted));

var response = getCompleted.get(TIMEOUT, TimeUnit.SECONDS);
var responseBody = response.getJson();
var allHoldings = JsonArrayHelper.toList(responseBody.getJsonArray("holdingsRecords"));

assertThat(allHoldings.size(), is(3));
assertThat(responseBody.getInteger("totalRecords"), is(3));

assertThat(allHoldings.stream().anyMatch(filterById(firstHoldingId)), is(true));
assertThat(allHoldings.stream().anyMatch(filterById(secondHoldingId)), is(true));
assertThat(allHoldings.stream().anyMatch(filterById(thirdHoldingId)), is(true));
}

@Test
public void cannotPageWithNegativeLimit() throws Exception {
UUID instanceId = UUID.randomUUID();
Expand Down
57 changes: 57 additions & 0 deletions src/test/java/org/folio/rest/api/InstanceStorageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,63 @@ public void canGetAllInstances() throws InterruptedException, ExecutionException
hasItem(identifierMatches(UUID_ASIN.toString(), "B01D1PLMDO")));
}

@Test
public void canRetrieveAllInstances() throws InterruptedException, ExecutionException, TimeoutException {
var firstInstanceId = UUID.randomUUID();
var firstInstanceToCreate = smallAngryPlanet(firstInstanceId);
var secondInstanceId = UUID.randomUUID();
var secondInstanceToCreate = nod(secondInstanceId);

createInstance(firstInstanceToCreate);
createInstance(secondInstanceToCreate);

var query = "(cql.allRecords=1) sortBy title";
var retrieveCompleted = new CompletableFuture<Response>();
var retrieveByTitleCompleted = new CompletableFuture<Response>();

getClient().post(instancesStorageUrl("/retrieve"), new JsonObject(), TENANT_ID,
json(retrieveCompleted));
getClient().post(instancesStorageUrl("/retrieve"), new JsonObject().put("query", query), TENANT_ID,
json(retrieveByTitleCompleted));

var retrieveBody = retrieveCompleted.get(10, SECONDS).getJson();
var allInstances = retrieveBody.getJsonArray(INSTANCES_KEY);

var retrieveByTitleBody = retrieveByTitleCompleted.get(10, SECONDS).getJson();
var sortedInstances = retrieveByTitleBody.getJsonArray(INSTANCES_KEY);

assertThat(allInstances.size(), is(2));
assertThat(sortedInstances.size(), is(2));
assertThat(retrieveBody.getInteger(TOTAL_RECORDS_KEY), is(2));

var firstInstance = allInstances.getJsonObject(0);
var secondInstance = allInstances.getJsonObject(1);
// no "sortBy" used so the database can return them in any order.
// swap if needed:
if (firstInstanceId.toString().equals(secondInstance.getString("id"))) {
var tmp = firstInstance;
firstInstance = secondInstance;
secondInstance = tmp;
}
final var sortedInstance = sortedInstances.getJsonObject(0);

assertThat(firstInstance.getString("id"), is(firstInstanceId.toString()));
assertThat(firstInstance.getString("title"), is("Long Way to a Small Angry Planet"));

assertThat(firstInstance.getJsonArray("identifiers").size(), is(1));
assertThat(firstInstance.getJsonArray("identifiers"),
hasItem(identifierMatches(UUID_ISBN.toString(), "9781473619777")));

assertThat(secondInstance.getString("id"), is(secondInstanceId.toString()));
assertThat(secondInstance.getString("title"), is("Nod"));

assertThat(secondInstance.getJsonArray("identifiers").size(), is(1));
assertThat(secondInstance.getJsonArray("identifiers"),
hasItem(identifierMatches(UUID_ASIN.toString(), "B01D1PLMDO")));

assertThat(sortedInstance.getString("title"), is("Long Way to a Small Angry Planet"));
}

@Test
public void canSearchByClassificationNumberWithoutArrayModifier()
throws InterruptedException, ExecutionException, TimeoutException {
Expand Down

0 comments on commit d2afdc9

Please sign in to comment.