diff --git a/NEWS.md b/NEWS.md index f6ebb7cdb..f0b9162f3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,12 +4,13 @@ * 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 domain event production for institution create/update/delete ([MODINVSTOR-1218](https://issues.folio.org/browse/MODINVSTOR-1218)) +* 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)) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b0da4556d..07f921fc1 100755 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -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}", @@ -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"] diff --git a/pom.xml b/pom.xml index d6c8b5f7a..0d3e42711 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ UTF-8 UTF-8 ${basedir}/ramls/ - /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 + /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 35.2.2 diff --git a/ramls/examples/retrieveEntitiesDto.json b/ramls/examples/retrieveEntitiesDto.json new file mode 100644 index 000000000..11df3e546 --- /dev/null +++ b/ramls/examples/retrieveEntitiesDto.json @@ -0,0 +1,5 @@ +{ + "limit": 10, + "offset": 10, + "query": "status=\"Available\"" +} diff --git a/ramls/holdings-storage.raml b/ramls/holdings-storage.raml index 813fc05c6..1accb70e4 100755 --- a/ramls/holdings-storage.raml +++ b/ramls/holdings-storage.raml @@ -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: @@ -81,4 +82,15 @@ resourceTypes: type: holdingsRecord example: strict: false - value: !include examples/holdings-storage/holdingsRecord_get.json \ No newline at end of file + 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 diff --git a/ramls/instance-storage.raml b/ramls/instance-storage.raml index 09e0e3343..968a24439 100644 --- a/ramls/instance-storage.raml +++ b/ramls/instance-storage.raml @@ -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 @@ -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 diff --git a/ramls/retrieveEntitiesDto.json b/ramls/retrieveEntitiesDto.json new file mode 100644 index 000000000..cbb0783b8 --- /dev/null +++ b/ramls/retrieveEntitiesDto.json @@ -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" + } + } +} diff --git a/src/main/java/org/folio/rest/impl/HoldingsStorageApi.java b/src/main/java/org/folio/rest/impl/HoldingsStorageApi.java index 5c665ba26..82bfde2a1 100644 --- a/src/main/java/org/folio/rest/impl/HoldingsStorageApi.java +++ b/src/main/java/org/folio/rest/impl/HoldingsStorageApi.java @@ -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; @@ -85,6 +86,17 @@ public void deleteHoldingsStorageHoldingsByHoldingsRecordId( .onComplete(asyncResultHandler); } + @Validate + @Override + public void postHoldingsStorageHoldingsRetrieve(RetrieveDto entity, + RoutingContext routingContext, + Map okapiHeaders, + Handler> 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( diff --git a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java index 9ef8a103e..ed78f7684 100644 --- a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java @@ -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; @@ -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 @@ -200,22 +202,7 @@ public void getInstanceStorageInstances(String totalRecords, int offset, int lim Handler> 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 @@ -387,6 +374,40 @@ public void putInstanceStorageInstancesSourceRecordModsByInstanceId( .respond500WithTextPlain("Not implemented yet."))); } + @Validate + @Override + public void postInstanceStorageInstancesRetrieve(RetrieveDto entity, + RoutingContext routingContext, + Map okapiHeaders, + Handler> 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 okapiHeaders, + Handler> 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); } diff --git a/src/test/java/org/folio/rest/api/HoldingsStorageTest.java b/src/test/java/org/folio/rest/api/HoldingsStorageTest.java index 3979e77e8..31cd11663 100644 --- a/src/test/java/org/folio/rest/api/HoldingsStorageTest.java +++ b/src/test/java/org/folio/rest/api/HoldingsStorageTest.java @@ -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 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(); diff --git a/src/test/java/org/folio/rest/api/InstanceStorageTest.java b/src/test/java/org/folio/rest/api/InstanceStorageTest.java index 97705eecc..ef8a9bd97 100644 --- a/src/test/java/org/folio/rest/api/InstanceStorageTest.java +++ b/src/test/java/org/folio/rest/api/InstanceStorageTest.java @@ -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(); + var retrieveByTitleCompleted = new CompletableFuture(); + + 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 {