From 6d09dae49b39edfc1348661b56f236738f496e76 Mon Sep 17 00:00:00 2001
From: Saba-Zedginidze-EPAM
<148070844+Saba-Zedginidze-EPAM@users.noreply.github.com>
Date: Wed, 28 Aug 2024 19:59:02 +0400
Subject: [PATCH 1/2] [MODINVSTOR-1243] Implement endpoint to retrieve items
from multiple tenants (#755)
* [MODINVSTOR-1243] Implement endpoint to retrieve items from multiple tenants
---
descriptors/ModuleDescriptor-template.json | 14 ++
pom.xml | 2 +
ramls/inventory.raml | 13 ++
ramls/tenantItemPair.json | 18 +++
ramls/tenantItemPairCollection.json | 17 +++
.../folio/inventory/InventoryVerticle.java | 2 +
.../inventory/resources/TenantItems.java | 142 ++++++++++++++++++
src/test/java/api/ApiTestSuite.java | 4 +-
.../java/api/items/TenantItemApiTests.java | 107 +++++++++++++
src/test/java/api/support/ApiRoot.java | 6 +
src/test/java/support/fakes/FakeOkapi.java | 9 ++
11 files changed, 333 insertions(+), 1 deletion(-)
create mode 100644 ramls/tenantItemPair.json
create mode 100644 ramls/tenantItemPairCollection.json
create mode 100644 src/main/java/org/folio/inventory/resources/TenantItems.java
create mode 100644 src/test/java/api/items/TenantItemApiTests.java
diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 4b65f2502..5bc3ee80b 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -365,6 +365,14 @@
"inventory-storage.instances.item.get"
]
},
+ {
+ "methods": ["POST"],
+ "pathPattern": "/inventory/tenant-items",
+ "permissionsRequired": ["inventory.tenant-items.collection.get"],
+ "modulePermissions": [
+ "inventory-storage.items.collection.get"
+ ]
+ },
{
"methods": ["GET"],
"pathPattern": "/inventory/instances/{id}",
@@ -681,6 +689,11 @@
"displayName": "Inventory - get item collection",
"description": "Get item collection"
},
+ {
+ "permissionName": "inventory.tenant-items.collection.get",
+ "displayName": "Inventory - get item collection from multiple tenants",
+ "description": "Get item collection from multiple tenants"
+ },
{
"permissionName": "inventory.items.collection.delete",
"displayName": "Inventory - delete entire item collection",
@@ -828,6 +841,7 @@
"description": "Entire set of permissions needed to use the inventory",
"subPermissions": [
"inventory.items.collection.get",
+ "inventory.tenant-items.collection.get",
"inventory.items.item.get",
"inventory.items.item.post",
"inventory.items.item.put",
diff --git a/pom.xml b/pom.xml
index 228b5b9f5..49b91ca8a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -449,6 +449,8 @@
${basedir}/ramls/items_update_ownership.json
${basedir}/ramls/update_ownership_response.json
${basedir}/ramls/instance-ingress-event.json
+ ${basedir}/ramls/tenantItemPair.json
+ ${basedir}/ramls/tenantItemPairCollection.json
org.folio
true
diff --git a/ramls/inventory.raml b/ramls/inventory.raml
index 97a7bda50..0b780b512 100644
--- a/ramls/inventory.raml
+++ b/ramls/inventory.raml
@@ -15,6 +15,7 @@ types:
holdings: !include holdings-record.json
instance: !include instance.json
instances: !include instances.json
+ tenantItemPairCollection: !include tenantItemPairCollection.json
traits:
language: !include raml-util/traits/language.raml
@@ -294,6 +295,18 @@ resourceTypes:
Possible values of the 'relations' parameter are: 'onlyBoundWiths', 'onlyBoundWithsSkipDirectlyLinkedItem'",
example: "holdingsRecordId==\"[UUID]\""}
]
+ /tenant-items:
+ displayName: Fetch items based on tenant IDs
+ post:
+ body:
+ application/json:
+ type: tenantItemPairCollection
+ responses:
+ 200:
+ description: "Fetched items based on tenant IDs"
+ body:
+ application/json:
+ type: items
/holdings/{holdingsId}:
put:
description: Update Holdings by holdingsId
diff --git a/ramls/tenantItemPair.json b/ramls/tenantItemPair.json
new file mode 100644
index 000000000..0573be0e8
--- /dev/null
+++ b/ramls/tenantItemPair.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Pair of item and tenant IDs",
+ "type": "object",
+ "properties": {
+ "tenantId": {
+ "type": "string",
+ "description": "Unique ID of the tenant where the item is located"
+ },
+ "itemId": {
+ "type": "string",
+ "description": "Unique ID (UUID) of the item",
+ "$ref": "uuid.json"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["itemId", "tenantId"]
+}
diff --git a/ramls/tenantItemPairCollection.json b/ramls/tenantItemPairCollection.json
new file mode 100644
index 000000000..390c7063a
--- /dev/null
+++ b/ramls/tenantItemPairCollection.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Collection of pairs of item and tenant IDs",
+ "type": "object",
+ "properties": {
+ "tenantItemPairs": {
+ "type": "array",
+ "description": "Pairs of tenantId and itemId",
+ "items": {
+ "type": "object",
+ "$ref": "tenantItemPair.json"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": ["tenantItemPairs"]
+}
diff --git a/src/main/java/org/folio/inventory/InventoryVerticle.java b/src/main/java/org/folio/inventory/InventoryVerticle.java
index 80f2bdae9..1cc59b389 100644
--- a/src/main/java/org/folio/inventory/InventoryVerticle.java
+++ b/src/main/java/org/folio/inventory/InventoryVerticle.java
@@ -19,6 +19,7 @@
import org.folio.inventory.resources.ItemsByHoldingsRecordId;
import org.folio.inventory.resources.MoveApi;
import org.folio.inventory.resources.TenantApi;
+import org.folio.inventory.resources.TenantItems;
import org.folio.inventory.resources.UpdateOwnershipApi;
import org.folio.inventory.storage.Storage;
@@ -71,6 +72,7 @@ public void start(Promise started) {
new InventoryConfigApi().register(router);
new TenantApi().register(router);
new UpdateOwnershipApi(storage, client, consortiumService).register(router);
+ new TenantItems(client).register(router);
Handler> onHttpServerStart = result -> {
if (result.succeeded()) {
diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java
new file mode 100644
index 000000000..c6079b6f4
--- /dev/null
+++ b/src/main/java/org/folio/inventory/resources/TenantItems.java
@@ -0,0 +1,142 @@
+package org.folio.inventory.resources;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+import static org.folio.inventory.support.CqlHelper.multipleRecordsCqlQuery;
+
+import java.lang.invoke.MethodHandles;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.http.HttpStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.folio.TenantItemPair;
+import org.folio.TenantItemPairCollection;
+import org.folio.inventory.common.WebContext;
+import org.folio.inventory.storage.external.CollectionResourceClient;
+import org.folio.inventory.support.JsonArrayHelper;
+import org.folio.inventory.support.http.client.OkapiHttpClient;
+import org.folio.inventory.support.http.client.Response;
+import org.folio.inventory.support.http.server.JsonResponse;
+import org.folio.inventory.support.http.server.ServerErrorResponse;
+
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.handler.BodyHandler;
+
+/**
+ * Resource that allows to get Inventory items from multiple tenants at once.
+ * User should have an affiliation in order to be able to retrieve items from the corresponding tenant.
+ */
+public class TenantItems {
+
+ private static final Logger LOG = LogManager.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items";
+ public static final String ITEMS_FIELD = "items";
+ public static final String TOTAL_RECORDS_FIELD = "totalRecords";
+ public static final String TENANT_ID_FIELD = "tenantId";
+
+ private final HttpClient client;
+
+ public TenantItems(HttpClient client) {
+ this.client = client;
+ }
+
+ public void register(Router router) {
+ router.post(TENANT_ITEMS_PATH + "*").handler(BodyHandler.create());
+ router.post(TENANT_ITEMS_PATH).handler(this::getItemsFromTenants);
+ }
+
+ /**
+ * This API is meant to be used by UI to fetch different items from several
+ * tenants together within one call
+ *
+ */
+ private void getItemsFromTenants(RoutingContext routingContext) {
+ var getItemsFutures = routingContext.body().asPojo(TenantItemPairCollection.class)
+ .getTenantItemPairs().stream()
+ .collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getItemId, toList())))
+ .entrySet().stream()
+ .map(tenantToItems -> getItemsWithTenantId(tenantToItems.getKey(), tenantToItems.getValue(), routingContext))
+ .toList();
+
+ CompletableFuture.allOf(getItemsFutures.toArray(new CompletableFuture[0]))
+ .thenApply(v -> getItemsFutures.stream()
+ .map(CompletableFuture::join)
+ .flatMap(List::stream)
+ .toList())
+ .thenApply(this::constructResponse)
+ .thenAccept(jsonObject -> JsonResponse.success(routingContext.response(), jsonObject));
+ }
+
+ private CompletableFuture> getItemsWithTenantId(String tenantId, List itemIds, RoutingContext routingContext) {
+ LOG.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId);
+ var context = new WebContext(routingContext);
+ CollectionResourceClient itemsStorageClient;
+ try {
+ OkapiHttpClient okapiClient = createHttpClient(tenantId, context, routingContext);
+ itemsStorageClient = createItemsStorageClient(okapiClient, context);
+ }
+ catch (MalformedURLException e) {
+ invalidOkapiUrlResponse(routingContext, context);
+ return CompletableFuture.completedFuture(List.of());
+ }
+
+ var getByIdsQuery = multipleRecordsCqlQuery(itemIds);
+ var itemsFetched = new CompletableFuture();
+ itemsStorageClient.getAll(getByIdsQuery, itemsFetched::complete);
+
+ return itemsFetched.thenApplyAsync(response ->
+ getItemsWithTenantId(tenantId, response));
+ }
+
+ private List getItemsWithTenantId(String tenantId, Response response) {
+ if (response.getStatusCode() != HttpStatus.SC_OK || !response.hasBody()) {
+ return List.of();
+ }
+ return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD).stream()
+ .map(item -> item.put(TENANT_ID_FIELD, tenantId))
+ .toList();
+ }
+
+ private JsonObject constructResponse(List items) {
+ return JsonObject.of(
+ ITEMS_FIELD, JsonArray.of(items.toArray()),
+ TOTAL_RECORDS_FIELD, items.size()
+ );
+ }
+
+ private CollectionResourceClient createItemsStorageClient(OkapiHttpClient client, WebContext context) throws MalformedURLException {
+ return new CollectionResourceClient(client, new URL(context.getOkapiLocation() + "/item-storage/items"));
+ }
+
+ private OkapiHttpClient createHttpClient(String tenantId, WebContext context,
+ RoutingContext routingContext) throws MalformedURLException {
+ return new OkapiHttpClient(WebClient.wrap(client),
+ URI.create(context.getOkapiLocation()).toURL(),
+ Optional.ofNullable(tenantId).orElse(context.getTenantId()),
+ context.getToken(),
+ context.getUserId(),
+ context.getRequestId(),
+ exception -> ServerErrorResponse.internalError(routingContext.response(),
+ format("Failed to contact storage module: %s", exception.toString())));
+ }
+
+ private void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) {
+ ServerErrorResponse.internalError(routingContext.response(),
+ String.format("Invalid Okapi URL: %s", context.getOkapiLocation()));
+ }
+
+}
diff --git a/src/test/java/api/ApiTestSuite.java b/src/test/java/api/ApiTestSuite.java
index ccdf921b3..61f3a6b37 100644
--- a/src/test/java/api/ApiTestSuite.java
+++ b/src/test/java/api/ApiTestSuite.java
@@ -39,6 +39,7 @@
import api.items.MarkItemUnavailableApiTests;
import api.items.MarkItemUnknownApiTests;
import api.items.MarkItemWithdrawnApiTests;
+import api.items.TenantItemApiTests;
import api.support.ControlledVocabularyPreparation;
import api.support.http.ResourceClient;
import io.vertx.core.json.JsonArray;
@@ -72,7 +73,8 @@
AdminApiTest.class,
InventoryConfigApiTest.class,
HoldingsUpdateOwnershipApiTest.class,
- ItemUpdateOwnershipApiTest.class
+ ItemUpdateOwnershipApiTest.class,
+ TenantItemApiTests.class
})
public class ApiTestSuite {
public static final int INVENTORY_VERTICLE_TEST_PORT = 9603;
diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java
new file mode 100644
index 000000000..4b0095bcd
--- /dev/null
+++ b/src/test/java/api/items/TenantItemApiTests.java
@@ -0,0 +1,107 @@
+package api.items;
+
+import static api.ApiTestSuite.COLLEGE_TENANT_ID;
+import static api.ApiTestSuite.CONSORTIA_TENANT_ID;
+import static api.ApiTestSuite.getBookMaterialType;
+import static api.ApiTestSuite.getCanCirculateLoanType;
+import static api.support.InstanceSamples.smallAngryPlanet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.folio.inventory.resources.TenantItems.ITEMS_FIELD;
+import static org.folio.inventory.resources.TenantItems.TENANT_ID_FIELD;
+import static org.folio.inventory.resources.TenantItems.TOTAL_RECORDS_FIELD;
+import static org.folio.inventory.support.ItemUtil.ID;
+
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.folio.TenantItemPair;
+import org.folio.TenantItemPairCollection;
+import org.folio.inventory.support.JsonArrayHelper;
+import org.folio.inventory.support.http.client.OkapiHttpClient;
+import org.folio.inventory.support.http.client.Response;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import api.support.ApiRoot;
+import api.support.ApiTests;
+import api.support.InstanceApiClient;
+import api.support.builders.HoldingRequestBuilder;
+import api.support.http.ResourceClient;
+import io.vertx.core.json.JsonObject;
+import junitparams.JUnitParamsRunner;
+
+@RunWith(JUnitParamsRunner.class)
+public class TenantItemApiTests extends ApiTests {
+
+ @Test
+ public void testTenantItemsGetFromDifferentTenants() throws MalformedURLException,
+ ExecutionException, InterruptedException, TimeoutException {
+
+ var consortiumItemId = createConsortiumInstanceHoldingItem();
+ var collegeItemId = createCollegeInstanceHoldingItem();
+ var consortiumItem = consortiumItemsClient.getById(consortiumItemId).getJson();
+ var collegeItem = collegeItemsClient.getById(collegeItemId).getJson();
+
+ assertThat(consortiumItem.getString(ID)).matches(consortiumItemId.toString());
+ assertThat(collegeItem.getString(ID)).matches(collegeItemId.toString());
+
+ var tenantItemPairCollection = constructTenantItemPairCollection(Map.of(
+ CONSORTIA_TENANT_ID, consortiumItem.getString(ID),
+ COLLEGE_TENANT_ID, collegeItem.getString(ID)
+ ));
+
+ var response = okapiClient.post(ApiRoot.tenantItems(), JsonObject.mapFrom(tenantItemPairCollection))
+ .toCompletableFuture().get(5, TimeUnit.SECONDS);
+ assertThat(response.getStatusCode()).isEqualTo(200);
+
+ consortiumItem.put(TENANT_ID_FIELD, CONSORTIA_TENANT_ID);
+ collegeItem.put(TENANT_ID_FIELD, COLLEGE_TENANT_ID);
+ var items = extractItems(response, 2);
+ assertThat(items).contains(consortiumItem, collegeItem);
+ }
+
+ private UUID createConsortiumInstanceHoldingItem() {
+ return createInstanceHoldingItem(consortiumItemsClient, consortiumHoldingsStorageClient, consortiumOkapiClient);
+ }
+
+ private UUID createCollegeInstanceHoldingItem() {
+ return createInstanceHoldingItem(collegeItemsClient, collegeHoldingsStorageClient, collegeOkapiClient);
+ }
+
+ private UUID createInstanceHoldingItem(ResourceClient itemsStorageClient, ResourceClient holdingsStorageClient, OkapiHttpClient okapiHttpClient) {
+ var instanceId = UUID.randomUUID();
+ InstanceApiClient.createInstance(okapiHttpClient, smallAngryPlanet(instanceId));
+ var holdingId = holdingsStorageClient.create(new HoldingRequestBuilder()
+ .forInstance(instanceId)).getId();
+ var itemId = UUID.randomUUID();
+ var newItemRequest = JsonObject.of(
+ "id", itemId.toString(),
+ "status", new JsonObject().put("name", "Available"),
+ "holdingsRecordId", holdingId,
+ "materialTypeId", getBookMaterialType(),
+ "permanentLoanTypeId", getCanCirculateLoanType());
+ itemsStorageClient.create(newItemRequest);
+ return itemId;
+ }
+
+ private List extractItems(Response itemsResponse, int expected) {
+ var itemsCollection = itemsResponse.getJson();
+ var items = JsonArrayHelper.toList(itemsCollection.getJsonArray(ITEMS_FIELD));
+ assertThat(items).hasSize(expected);
+ assertThat(itemsCollection.getInteger(TOTAL_RECORDS_FIELD)).isEqualTo(expected);
+ return items;
+ }
+
+ private TenantItemPairCollection constructTenantItemPairCollection(Map tenantsToItemIds) {
+ return new TenantItemPairCollection()
+ .withTenantItemPairs(tenantsToItemIds.entrySet().stream()
+ .map(pair -> new TenantItemPair().withTenantId(pair.getKey()).withItemId(pair.getValue()))
+ .toList());
+ }
+
+}
diff --git a/src/test/java/api/support/ApiRoot.java b/src/test/java/api/support/ApiRoot.java
index aa1373cc1..cac7fc07f 100644
--- a/src/test/java/api/support/ApiRoot.java
+++ b/src/test/java/api/support/ApiRoot.java
@@ -71,6 +71,12 @@ public static URL items(String query)
return new URL(String.format("%s/items?%s", inventory(), query));
}
+ public static URL tenantItems()
+ throws MalformedURLException {
+
+ return new URL(String.format("%s/tenant-items", inventory()));
+ }
+
public static String isbn() {
return String.format("%s/isbn", ApiTestSuite.apiRoot());
}
diff --git a/src/test/java/support/fakes/FakeOkapi.java b/src/test/java/support/fakes/FakeOkapi.java
index ae75b0210..542a7b1bd 100644
--- a/src/test/java/support/fakes/FakeOkapi.java
+++ b/src/test/java/support/fakes/FakeOkapi.java
@@ -29,6 +29,7 @@ public void start(Promise startFuture) {
registerFakeHoldingStorageModule(router);
registerFakeAuthorityStorageModule(router);
registerFakeItemsStorageModule(router);
+ registerFakeTenantItemsStorageModule(router);
registerFakeMaterialTypesModule(router);
registerFakeLoanTypesModule(router);
registerFakeLocationsModule(router);
@@ -171,6 +172,14 @@ private void registerFakeItemsStorageModule(Router router) {
.create().register(router);
}
+ private void registerFakeTenantItemsStorageModule(Router router) {
+ new FakeStorageModuleBuilder()
+ .withRecordName("tenantItem")
+ .withRootPath("/inventory/tenant-items")
+ .withCollectionPropertyName("items")
+ .create().register(router);
+ }
+
private void registerFakeMaterialTypesModule(Router router) {
new FakeStorageModuleBuilder()
From e450f3f6d64e585ba641039f3aba82b287e0da34 Mon Sep 17 00:00:00 2001
From: Saba-Zedginidze-EPAM
<148070844+Saba-Zedginidze-EPAM@users.noreply.github.com>
Date: Thu, 29 Aug 2024 18:41:29 +0400
Subject: [PATCH 2/2] [MODINV-1070] Modify TenantItems response schema (#759)
* [MODINV-1070] Modify TenantItems response schema
* [MODINV-1070] Fix failing test
* [MODINV-1070] Remove unused import
* [MODINV-1070] Remove unused import
---
pom.xml | 1 +
ramls/tenantItemResponse.json | 20 ++++++++++++
.../inventory/resources/TenantItems.java | 32 +++++++++----------
.../java/api/items/TenantItemApiTests.java | 10 +++---
4 files changed, 42 insertions(+), 21 deletions(-)
create mode 100644 ramls/tenantItemResponse.json
diff --git a/pom.xml b/pom.xml
index 49b91ca8a..705f4f467 100644
--- a/pom.xml
+++ b/pom.xml
@@ -451,6 +451,7 @@
${basedir}/ramls/instance-ingress-event.json
${basedir}/ramls/tenantItemPair.json
${basedir}/ramls/tenantItemPairCollection.json
+ ${basedir}/ramls/tenantItemResponse.json
org.folio
true
diff --git a/ramls/tenantItemResponse.json b/ramls/tenantItemResponse.json
new file mode 100644
index 000000000..8bdf521e3
--- /dev/null
+++ b/ramls/tenantItemResponse.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Collection of pairs of item and tenant IDs",
+ "type": "object",
+ "properties": {
+ "tenantItems": {
+ "type": "array",
+ "description": "Items with corresponding tenantIds",
+ "items": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ },
+ "totalRecords": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["item", "totalRecords"]
+}
diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java
index c6079b6f4..c02ac570d 100644
--- a/src/main/java/org/folio/inventory/resources/TenantItems.java
+++ b/src/main/java/org/folio/inventory/resources/TenantItems.java
@@ -17,8 +17,10 @@
import org.apache.http.HttpStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.folio.TenantItem;
import org.folio.TenantItemPair;
import org.folio.TenantItemPairCollection;
+import org.folio.TenantItemResponse;
import org.folio.inventory.common.WebContext;
import org.folio.inventory.storage.external.CollectionResourceClient;
import org.folio.inventory.support.JsonArrayHelper;
@@ -28,7 +30,6 @@
import org.folio.inventory.support.http.server.ServerErrorResponse;
import io.vertx.core.http.HttpClient;
-import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
@@ -45,6 +46,7 @@ public class TenantItems {
private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items";
public static final String ITEMS_FIELD = "items";
+ public static final String ITEM_FIELD = "item";
public static final String TOTAL_RECORDS_FIELD = "totalRecords";
public static final String TENANT_ID_FIELD = "tenantId";
@@ -77,11 +79,11 @@ private void getItemsFromTenants(RoutingContext routingContext) {
.map(CompletableFuture::join)
.flatMap(List::stream)
.toList())
- .thenApply(this::constructResponse)
- .thenAccept(jsonObject -> JsonResponse.success(routingContext.response(), jsonObject));
+ .thenApply(items -> new TenantItemResponse().withTenantItems(items).withTotalRecords(items.size()))
+ .thenAccept(response -> JsonResponse.success(routingContext.response(), JsonObject.mapFrom(response)));
}
- private CompletableFuture> getItemsWithTenantId(String tenantId, List itemIds, RoutingContext routingContext) {
+ private CompletableFuture> getItemsWithTenantId(String tenantId, List itemIds, RoutingContext routingContext) {
LOG.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId);
var context = new WebContext(routingContext);
CollectionResourceClient itemsStorageClient;
@@ -98,24 +100,20 @@ private CompletableFuture> getItemsWithTenantId(String tenantId
var itemsFetched = new CompletableFuture();
itemsStorageClient.getAll(getByIdsQuery, itemsFetched::complete);
- return itemsFetched.thenApplyAsync(response ->
- getItemsWithTenantId(tenantId, response));
+ return itemsFetched
+ .thenApply(this::getItems)
+ .thenApply(items -> items.stream()
+ .map(item -> new TenantItem()
+ .withAdditionalProperty(ITEM_FIELD, item)
+ .withAdditionalProperty(TENANT_ID_FIELD, tenantId))
+ .toList());
}
- private List getItemsWithTenantId(String tenantId, Response response) {
+ private List getItems(Response response) {
if (response.getStatusCode() != HttpStatus.SC_OK || !response.hasBody()) {
return List.of();
}
- return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD).stream()
- .map(item -> item.put(TENANT_ID_FIELD, tenantId))
- .toList();
- }
-
- private JsonObject constructResponse(List items) {
- return JsonObject.of(
- ITEMS_FIELD, JsonArray.of(items.toArray()),
- TOTAL_RECORDS_FIELD, items.size()
- );
+ return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD);
}
private CollectionResourceClient createItemsStorageClient(OkapiHttpClient client, WebContext context) throws MalformedURLException {
diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java
index 4b0095bcd..5471bda7a 100644
--- a/src/test/java/api/items/TenantItemApiTests.java
+++ b/src/test/java/api/items/TenantItemApiTests.java
@@ -6,7 +6,7 @@
import static api.ApiTestSuite.getCanCirculateLoanType;
import static api.support.InstanceSamples.smallAngryPlanet;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.folio.inventory.resources.TenantItems.ITEMS_FIELD;
+import static org.folio.inventory.resources.TenantItems.ITEM_FIELD;
import static org.folio.inventory.resources.TenantItems.TENANT_ID_FIELD;
import static org.folio.inventory.resources.TenantItems.TOTAL_RECORDS_FIELD;
import static org.folio.inventory.support.ItemUtil.ID;
@@ -38,6 +38,8 @@
@RunWith(JUnitParamsRunner.class)
public class TenantItemApiTests extends ApiTests {
+ private static final String TENANT_ITEMS_FIELD = "tenantItems";
+
@Test
public void testTenantItemsGetFromDifferentTenants() throws MalformedURLException,
ExecutionException, InterruptedException, TimeoutException {
@@ -59,8 +61,8 @@ public void testTenantItemsGetFromDifferentTenants() throws MalformedURLExceptio
.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertThat(response.getStatusCode()).isEqualTo(200);
- consortiumItem.put(TENANT_ID_FIELD, CONSORTIA_TENANT_ID);
- collegeItem.put(TENANT_ID_FIELD, COLLEGE_TENANT_ID);
+ consortiumItem = JsonObject.of(ITEM_FIELD, consortiumItem, TENANT_ID_FIELD, CONSORTIA_TENANT_ID);
+ collegeItem = JsonObject.of(ITEM_FIELD, collegeItem, TENANT_ID_FIELD, COLLEGE_TENANT_ID);
var items = extractItems(response, 2);
assertThat(items).contains(consortiumItem, collegeItem);
}
@@ -91,7 +93,7 @@ private UUID createInstanceHoldingItem(ResourceClient itemsStorageClient, Resour
private List extractItems(Response itemsResponse, int expected) {
var itemsCollection = itemsResponse.getJson();
- var items = JsonArrayHelper.toList(itemsCollection.getJsonArray(ITEMS_FIELD));
+ var items = JsonArrayHelper.toList(itemsCollection.getJsonArray(TENANT_ITEMS_FIELD));
assertThat(items).hasSize(expected);
assertThat(itemsCollection.getInteger(TOTAL_RECORDS_FIELD)).isEqualTo(expected);
return items;