From 8288e2688c886c59dff728feb618bc3e375498de Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Fri, 23 Aug 2024 17:30:01 +0400 Subject: [PATCH 01/16] [MODINVSTOR-1243] Implement endpoint to retrieve items from multiple tenants --- descriptors/ModuleDescriptor-template.json | 8 ++ ramls/inventory.raml | 10 ++ ramls/tenantItemPair.json | 18 +++ ramls/tenantItemPairCollection.json | 17 +++ .../folio/inventory/InventoryVerticle.java | 2 + .../org/folio/inventory/resources/Items.java | 4 +- .../inventory/resources/TenantItems.java | 117 ++++++++++++++++++ 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 ramls/tenantItemPair.json create mode 100644 ramls/tenantItemPairCollection.json create mode 100644 src/main/java/org/folio/inventory/resources/TenantItems.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4b65f2502..2d64de3eb 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -365,6 +365,14 @@ "inventory-storage.instances.item.get" ] }, + { + "methods": ["GET"], + "pathPattern": "/inventory/tenant-items", + "permissionsRequired": ["inventory.items.collection.get"], + "modulePermissions": [ + "inventory-storage.items.collection.get" + ] + }, { "methods": ["GET"], "pathPattern": "/inventory/instances/{id}", diff --git a/ramls/inventory.raml b/ramls/inventory.raml index 97a7bda50..f3f71a0a0 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,15 @@ 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" /holdings/{holdingsId}: put: description: Update Holdings by holdingsId diff --git a/ramls/tenantItemPair.json b/ramls/tenantItemPair.json new file mode 100644 index 000000000..15b18b7e7 --- /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": { + "itemId": { + "type": "string", + "description": "Unique ID (UUID) of the item", + "$ref": "../../mod-inventory-storage/ramls/uuid.json" + }, + "tenantId": { + "type": "string", + "description": "Unique ID of the tenant where the item is located" + } + }, + "additionalProperties": false, + "required": ["itemId", "tenantId"] +} diff --git a/ramls/tenantItemPairCollection.json b/ramls/tenantItemPairCollection.json new file mode 100644 index 000000000..b1cf58ae1 --- /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": { + "itemTenantPairs": { + "type": "array", + "description": "Unique ID (UUID) of the item", + "items": { + "type": "object", + "$ref": "tenantItemPair.json" + } + } + }, + "additionalProperties": false, + "required": ["itemTenantPairs"] +} diff --git a/src/main/java/org/folio/inventory/InventoryVerticle.java b/src/main/java/org/folio/inventory/InventoryVerticle.java index 80f2bdae9..d76f01ea0 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(storage, client).register(router); Handler> onHttpServerStart = result -> { if (result.succeeded()) { diff --git a/src/main/java/org/folio/inventory/resources/Items.java b/src/main/java/org/folio/inventory/resources/Items.java index 17c1ac4a4..a3f19a79c 100644 --- a/src/main/java/org/folio/inventory/resources/Items.java +++ b/src/main/java/org/folio/inventory/resources/Items.java @@ -508,7 +508,7 @@ private OkapiHttpClient createHttpClient( exception.toString()))); } - private CollectionResourceClient createItemsStorageClient( + protected CollectionResourceClient createItemsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { @@ -718,7 +718,7 @@ private void respondWithItemRepresentation ( }); } - private void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) { + protected void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) { ServerErrorResponse.internalError(routingContext.response(), String.format("Invalid Okapi URL: %s", context.getOkapiLocation())); } 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..7aa2e813c --- /dev/null +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -0,0 +1,117 @@ +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.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.inventory.common.WebContext; +import org.folio.inventory.storage.Storage; +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; + +public class TenantItems extends Items { + + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items"; + private static final String TENANT_ITEM_PAIRS_FIELD = "itemTenantPairs"; + private static final String ITEMS_FIELD = "items"; + private static final String TOTAL_RECORDS_FIELD = "items"; + private static final String ITEM_ID_FIELD = "itemId"; + private static final String TENANT_ID_FIELD = "tenantId"; + + public TenantItems(final Storage storage, final HttpClient client) { + super(storage, client); + } + + @Override + public void register(Router router) { + 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 = JsonArrayHelper.toList(routingContext.body().asJsonObject(), TENANT_ITEM_PAIRS_FIELD).stream() + .collect(groupingBy(json -> json.getString(TENANT_ID_FIELD), mapping(json -> json.getString(ITEM_ID_FIELD), 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) + .collect(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 -> + response.getStatusCode() == 200 && response.hasBody() + ? JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD) + : List.of()); + } + + private JsonObject constructResponse(List items) { + return JsonObject.of( + ITEMS_FIELD, JsonArray.of(items.toArray()), + TOTAL_RECORDS_FIELD, items.size() + ); + } + + 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()))); + } + +} From fbdc18a5241a400c0cf7d03aa509200be8647b55 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Fri, 23 Aug 2024 17:42:52 +0400 Subject: [PATCH 02/16] [MODINVSTOR-1243] Compile new schemas to parse the request body --- pom.xml | 2 ++ .../org/folio/inventory/resources/TenantItems.java | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) 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/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index 7aa2e813c..4714ce3b0 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -15,6 +15,8 @@ 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.Storage; import org.folio.inventory.storage.external.CollectionResourceClient; @@ -36,11 +38,8 @@ public class TenantItems extends Items { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items"; - private static final String TENANT_ITEM_PAIRS_FIELD = "itemTenantPairs"; private static final String ITEMS_FIELD = "items"; private static final String TOTAL_RECORDS_FIELD = "items"; - private static final String ITEM_ID_FIELD = "itemId"; - private static final String TENANT_ID_FIELD = "tenantId"; public TenantItems(final Storage storage, final HttpClient client) { super(storage, client); @@ -57,8 +56,9 @@ public void register(Router router) { * */ private void getItemsFromTenants(RoutingContext routingContext) { - var getItemsFutures = JsonArrayHelper.toList(routingContext.body().asJsonObject(), TENANT_ITEM_PAIRS_FIELD).stream() - .collect(groupingBy(json -> json.getString(TENANT_ID_FIELD), mapping(json -> json.getString(ITEM_ID_FIELD), toList()))) + var getItemsFutures = routingContext.body().asPojo(TenantItemPairCollection.class) + .getItemTenantPairs().stream() + .collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getTenantId, toList()))) .entrySet().stream() .map(tenantToItems -> getItemsWithTenantId(tenantToItems.getKey(), tenantToItems.getValue(), routingContext)) .toList(); From 26548fade958df9b1240e0accda236c6560eac29 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Fri, 23 Aug 2024 17:55:52 +0400 Subject: [PATCH 03/16] [MODINVSTOR-1243] Fix uuid path --- ramls/tenantItemPair.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ramls/tenantItemPair.json b/ramls/tenantItemPair.json index 15b18b7e7..5f84d2ffe 100644 --- a/ramls/tenantItemPair.json +++ b/ramls/tenantItemPair.json @@ -6,7 +6,7 @@ "itemId": { "type": "string", "description": "Unique ID (UUID) of the item", - "$ref": "../../mod-inventory-storage/ramls/uuid.json" + "$ref": "uuid.json" }, "tenantId": { "type": "string", From 71060306c08b531768ec5733b53076e26a3e8da0 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Fri, 23 Aug 2024 18:07:23 +0400 Subject: [PATCH 04/16] [MODINVSTOR-1243] Remove unnecessary collect --- src/main/java/org/folio/inventory/resources/TenantItems.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index 4714ce3b0..f129d9cdb 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -67,7 +67,7 @@ private void getItemsFromTenants(RoutingContext routingContext) { .thenApply(v -> getItemsFutures.stream() .map(CompletableFuture::join) .flatMap(List::stream) - .collect(toList())) + .toList()) .thenApply(this::constructResponse) .thenAccept(jsonObject -> JsonResponse.success(routingContext.response(), jsonObject)); } From 65f277ae1b24db9ae97a07f37b68576b6784575f Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 16:31:08 +0400 Subject: [PATCH 05/16] [MODINVSTOR-1243] Add integration test --- .../org/folio/inventory/resources/Items.java | 4 +- .../inventory/resources/TenantItems.java | 20 +++- .../java/api/items/TenantItemApiTests.java | 104 ++++++++++++++++++ 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/test/java/api/items/TenantItemApiTests.java diff --git a/src/main/java/org/folio/inventory/resources/Items.java b/src/main/java/org/folio/inventory/resources/Items.java index a3f19a79c..17c1ac4a4 100644 --- a/src/main/java/org/folio/inventory/resources/Items.java +++ b/src/main/java/org/folio/inventory/resources/Items.java @@ -508,7 +508,7 @@ private OkapiHttpClient createHttpClient( exception.toString()))); } - protected CollectionResourceClient createItemsStorageClient( + private CollectionResourceClient createItemsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { @@ -718,7 +718,7 @@ private void respondWithItemRepresentation ( }); } - protected void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) { + private void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) { ServerErrorResponse.internalError(routingContext.response(), String.format("Invalid Okapi URL: %s", context.getOkapiLocation())); } diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index f129d9cdb..abafbbf0e 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -9,6 +9,7 @@ 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; @@ -33,15 +34,15 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.WebClient; -public class TenantItems extends Items { +public class TenantItems extends AbstractInventoryResource { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); - private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items"; - private static final String ITEMS_FIELD = "items"; - private static final String TOTAL_RECORDS_FIELD = "items"; + public 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 TenantItems(final Storage storage, final HttpClient client) { + public TenantItems(Storage storage, HttpClient client) { super(storage, client); } @@ -102,6 +103,10 @@ private JsonObject constructResponse(List items) { ); } + 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), @@ -114,4 +119,9 @@ private OkapiHttpClient createHttpClient(String tenantId, WebContext context, 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/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java new file mode 100644 index 000000000..6c2ff9d0e --- /dev/null +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -0,0 +1,104 @@ +package api.items; + +import static api.ApiTestSuite.COLLEGE_TENANT_ID; +import static api.ApiTestSuite.CONSORTIA_TENANT_ID; +import static api.support.InstanceSamples.smallAngryPlanet; +import static java.util.concurrent.TimeUnit.SECONDS; +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_ITEMS_PATH; +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.builders.ItemRequestBuilder; +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 { + + createConsortiumInstanceHoldingItem(); + createCollegeInstanceHoldingItem(); + + var consortiumItem = getItems(consortiumOkapiClient, 1).get(0); + var collegeItem = getItems(collegeOkapiClient, 1).get(0); + + var tenantItemPariCollection = constructTenantItemPairCollection(Map.of( + CONSORTIA_TENANT_ID, consortiumItem.getString(ID), + COLLEGE_TENANT_ID, collegeItem.getString(ID) + )); + var response = okapiClient.post(TENANT_ITEMS_PATH, JsonObject.mapFrom(tenantItemPariCollection)) + .toCompletableFuture().get(5, TimeUnit.SECONDS); + assertThat(response.getStatusCode()).isEqualTo(200); + var items = extractItems(response, 2); + + assertThat(items).contains(consortiumItem, collegeItem); + } + + private void createConsortiumInstanceHoldingItem() { + createInstanceHoldingItem(consortiumItemsClient, consortiumHoldingsStorageClient, consortiumOkapiClient); + } + + private void createCollegeInstanceHoldingItem() { + createInstanceHoldingItem(collegeItemsClient, collegeHoldingsStorageClient, collegeOkapiClient); + } + + private void createInstanceHoldingItem(ResourceClient itemStorageClient, ResourceClient holdingsStorageClient, OkapiHttpClient okapiHttpClient) { + var instanceId = UUID.randomUUID(); + InstanceApiClient.createInstance(okapiHttpClient, smallAngryPlanet(instanceId)); + var holdingId = holdingsStorageClient.create(new HoldingRequestBuilder() + .forInstance(instanceId)).getId(); + itemStorageClient.create(new ItemRequestBuilder().forHolding(holdingId) + .book().canCirculate().withBarcode(String.valueOf(Math.random() * 100))); + } + + private List getItems(OkapiHttpClient okapiHttpClient, int expected) + throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { + + var itemsResponse = okapiHttpClient.get(ApiRoot.items()).toCompletableFuture().get(5, SECONDS); + assertThat(itemsResponse.getStatusCode()).isEqualTo(200); + + return extractItems(itemsResponse, expected); + } + + 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 itemToTenantIds) { + return new TenantItemPairCollection() + .withItemTenantPairs(itemToTenantIds.entrySet().stream() + .map(pair -> new TenantItemPair().withItemId(pair.getKey()).withTenantId(pair.getValue())) + .toList()); + } + +} From bb39dc5009f3446c3e1b0e8aa5ff85b4a608109e Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 16:50:39 +0400 Subject: [PATCH 06/16] [MODINVSTOR-1243] Attempt increase coverage --- .../java/org/folio/inventory/resources/TenantItems.java | 2 +- src/test/java/api/items/TenantItemApiTests.java | 3 +-- src/test/java/api/support/ApiRoot.java | 6 ++++++ src/test/java/support/fakes/FakeOkapi.java | 9 +++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index abafbbf0e..eb0df011f 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -38,7 +38,7 @@ public class TenantItems extends AbstractInventoryResource { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); - public static final String TENANT_ITEMS_PATH = "/inventory/tenant-items"; + 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"; diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java index 6c2ff9d0e..a22741006 100644 --- a/src/test/java/api/items/TenantItemApiTests.java +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -6,7 +6,6 @@ import static java.util.concurrent.TimeUnit.SECONDS; 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_ITEMS_PATH; import static org.folio.inventory.resources.TenantItems.TOTAL_RECORDS_FIELD; import static org.folio.inventory.support.ItemUtil.ID; @@ -52,7 +51,7 @@ public void testTenantItemsGetFromDifferentTenants() throws MalformedURLExceptio CONSORTIA_TENANT_ID, consortiumItem.getString(ID), COLLEGE_TENANT_ID, collegeItem.getString(ID) )); - var response = okapiClient.post(TENANT_ITEMS_PATH, JsonObject.mapFrom(tenantItemPariCollection)) + var response = okapiClient.post(ApiRoot.tenantItems(), JsonObject.mapFrom(tenantItemPariCollection)) .toCompletableFuture().get(5, TimeUnit.SECONDS); assertThat(response.getStatusCode()).isEqualTo(200); var items = extractItems(response, 2); 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 08dedeea30222297f09922b1cee397d736898551 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 16:52:30 +0400 Subject: [PATCH 07/16] [MODINVSTOR-1243] Include new test in suite --- src/test/java/api/ApiTestSuite.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; From 1047d5d65b65d7f11b89c359ab1891e4ec5d0957 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 17:20:12 +0400 Subject: [PATCH 08/16] [MODINVSTOR-1243] Remove unnecessary properties from item --- src/test/java/api/items/TenantItemApiTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java index a22741006..cb740b2c5 100644 --- a/src/test/java/api/items/TenantItemApiTests.java +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -73,7 +73,7 @@ private void createInstanceHoldingItem(ResourceClient itemStorageClient, Resourc var holdingId = holdingsStorageClient.create(new HoldingRequestBuilder() .forInstance(instanceId)).getId(); itemStorageClient.create(new ItemRequestBuilder().forHolding(holdingId) - .book().canCirculate().withBarcode(String.valueOf(Math.random() * 100))); + .withBarcode(String.valueOf(Math.random() * 100))); } private List getItems(OkapiHttpClient okapiHttpClient, int expected) From 952a8c7f2899f30208397c96bac555e1b6155fe3 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 17:26:29 +0400 Subject: [PATCH 09/16] [MODINVSTOR-1243] Fix http method in module descriptor --- descriptors/ModuleDescriptor-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 2d64de3eb..a1bac0693 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -366,7 +366,7 @@ ] }, { - "methods": ["GET"], + "methods": ["POST"], "pathPattern": "/inventory/tenant-items", "permissionsRequired": ["inventory.items.collection.get"], "modulePermissions": [ From e957735f02185e47a0a3dc37f51dbb37b6e4329d Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Mon, 26 Aug 2024 17:33:28 +0400 Subject: [PATCH 10/16] [MODINVSTOR-1243] Attempt increase coverage --- ramls/inventory.raml | 21 ++++---- ramls/tenantItemPairCollection.json | 4 +- .../folio/inventory/InventoryVerticle.java | 2 +- .../inventory/resources/TenantItems.java | 11 +++-- .../java/api/items/TenantItemApiTests.java | 49 ++++++++++--------- 5 files changed, 46 insertions(+), 41 deletions(-) diff --git a/ramls/inventory.raml b/ramls/inventory.raml index f3f71a0a0..0b780b512 100644 --- a/ramls/inventory.raml +++ b/ramls/inventory.raml @@ -295,15 +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" + /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/tenantItemPairCollection.json b/ramls/tenantItemPairCollection.json index b1cf58ae1..f5d34aef0 100644 --- a/ramls/tenantItemPairCollection.json +++ b/ramls/tenantItemPairCollection.json @@ -3,7 +3,7 @@ "description": "Collection of pairs of item and tenant IDs", "type": "object", "properties": { - "itemTenantPairs": { + "tenantItemPairs": { "type": "array", "description": "Unique ID (UUID) of the item", "items": { @@ -13,5 +13,5 @@ } }, "additionalProperties": false, - "required": ["itemTenantPairs"] + "required": ["tenantItemPairs"] } diff --git a/src/main/java/org/folio/inventory/InventoryVerticle.java b/src/main/java/org/folio/inventory/InventoryVerticle.java index d76f01ea0..1cc59b389 100644 --- a/src/main/java/org/folio/inventory/InventoryVerticle.java +++ b/src/main/java/org/folio/inventory/InventoryVerticle.java @@ -72,7 +72,7 @@ public void start(Promise started) { new InventoryConfigApi().register(router); new TenantApi().register(router); new UpdateOwnershipApi(storage, client, consortiumService).register(router); - new TenantItems(storage, client).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 index eb0df011f..be6eb1f1f 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -34,7 +34,7 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.WebClient; -public class TenantItems extends AbstractInventoryResource { +public class TenantItems { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); @@ -42,11 +42,12 @@ public class TenantItems extends AbstractInventoryResource { public static final String ITEMS_FIELD = "items"; public static final String TOTAL_RECORDS_FIELD = "totalRecords"; - public TenantItems(Storage storage, HttpClient client) { - super(storage, client); + private final HttpClient client; + + public TenantItems(HttpClient client) { + this.client = client; } - @Override public void register(Router router) { router.post(TENANT_ITEMS_PATH).handler(this::getItemsFromTenants); } @@ -58,7 +59,7 @@ public void register(Router router) { */ private void getItemsFromTenants(RoutingContext routingContext) { var getItemsFutures = routingContext.body().asPojo(TenantItemPairCollection.class) - .getItemTenantPairs().stream() + .getTenantItemPairs().stream() .collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getTenantId, toList()))) .entrySet().stream() .map(tenantToItems -> getItemsWithTenantId(tenantToItems.getKey(), tenantToItems.getValue(), routingContext)) diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java index cb740b2c5..5ad802f62 100644 --- a/src/test/java/api/items/TenantItemApiTests.java +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -2,8 +2,9 @@ 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 java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.folio.inventory.resources.TenantItems.ITEMS_FIELD; import static org.folio.inventory.resources.TenantItems.TOTAL_RECORDS_FIELD; @@ -29,7 +30,6 @@ import api.support.ApiTests; import api.support.InstanceApiClient; import api.support.builders.HoldingRequestBuilder; -import api.support.builders.ItemRequestBuilder; import api.support.http.ResourceClient; import io.vertx.core.json.JsonObject; import junitparams.JUnitParamsRunner; @@ -41,48 +41,49 @@ public class TenantItemApiTests extends ApiTests { public void testTenantItemsGetFromDifferentTenants() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { - createConsortiumInstanceHoldingItem(); - createCollegeInstanceHoldingItem(); + var consortiumItemId = createConsortiumInstanceHoldingItem(); + var collegeItemId = createCollegeInstanceHoldingItem(); + var consortiumItem = consortiumItemsClient.getById(consortiumItemId).getJson(); + var collegeItem = collegeItemsClient.getById(collegeItemId).getJson(); - var consortiumItem = getItems(consortiumOkapiClient, 1).get(0); - var collegeItem = getItems(collegeOkapiClient, 1).get(0); + assertThat(consortiumItem.getString(ID)).matches(consortiumItemId.toString()); + assertThat(collegeItem.getString(ID)).matches(collegeItemId.toString()); var tenantItemPariCollection = constructTenantItemPairCollection(Map.of( CONSORTIA_TENANT_ID, consortiumItem.getString(ID), COLLEGE_TENANT_ID, collegeItem.getString(ID) )); + var response = okapiClient.post(ApiRoot.tenantItems(), JsonObject.mapFrom(tenantItemPariCollection)) .toCompletableFuture().get(5, TimeUnit.SECONDS); assertThat(response.getStatusCode()).isEqualTo(200); - var items = extractItems(response, 2); + var items = extractItems(response, 2); assertThat(items).contains(consortiumItem, collegeItem); } - private void createConsortiumInstanceHoldingItem() { - createInstanceHoldingItem(consortiumItemsClient, consortiumHoldingsStorageClient, consortiumOkapiClient); + private UUID createConsortiumInstanceHoldingItem() { + return createInstanceHoldingItem(consortiumItemsClient, consortiumHoldingsStorageClient, consortiumOkapiClient); } - private void createCollegeInstanceHoldingItem() { - createInstanceHoldingItem(collegeItemsClient, collegeHoldingsStorageClient, collegeOkapiClient); + private UUID createCollegeInstanceHoldingItem() { + return createInstanceHoldingItem(collegeItemsClient, collegeHoldingsStorageClient, collegeOkapiClient); } - private void createInstanceHoldingItem(ResourceClient itemStorageClient, ResourceClient holdingsStorageClient, OkapiHttpClient okapiHttpClient) { + 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(); - itemStorageClient.create(new ItemRequestBuilder().forHolding(holdingId) - .withBarcode(String.valueOf(Math.random() * 100))); - } - - private List getItems(OkapiHttpClient okapiHttpClient, int expected) - throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { - - var itemsResponse = okapiHttpClient.get(ApiRoot.items()).toCompletableFuture().get(5, SECONDS); - assertThat(itemsResponse.getStatusCode()).isEqualTo(200); - - return extractItems(itemsResponse, expected); + 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) { @@ -95,7 +96,7 @@ private List extractItems(Response itemsResponse, int expected) { private TenantItemPairCollection constructTenantItemPairCollection(Map itemToTenantIds) { return new TenantItemPairCollection() - .withItemTenantPairs(itemToTenantIds.entrySet().stream() + .withTenantItemPairs(itemToTenantIds.entrySet().stream() .map(pair -> new TenantItemPair().withItemId(pair.getKey()).withTenantId(pair.getValue())) .toList()); } From cbd0c788456105d755d87219796db1424f86c10b Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Tue, 27 Aug 2024 16:21:38 +0400 Subject: [PATCH 11/16] [MODINVSTOR-1243] Add body handler --- src/main/java/org/folio/inventory/resources/TenantItems.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index be6eb1f1f..400d66f0e 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -33,6 +33,7 @@ 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; public class TenantItems { @@ -49,6 +50,7 @@ public TenantItems(HttpClient client) { } public void register(Router router) { + router.post(TENANT_ITEMS_PATH + "*").handler(BodyHandler.create()); router.post(TENANT_ITEMS_PATH).handler(this::getItemsFromTenants); } From d135a4e257e4fd4b5d8ba587ed7fa42759de2513 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Tue, 27 Aug 2024 16:46:21 +0400 Subject: [PATCH 12/16] [MODINVSTOR-1243] Fix typo --- ramls/tenantItemPair.json | 8 ++++---- ramls/tenantItemPairCollection.json | 2 +- .../org/folio/inventory/resources/TenantItems.java | 3 +-- src/test/java/api/items/TenantItemApiTests.java | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ramls/tenantItemPair.json b/ramls/tenantItemPair.json index 5f84d2ffe..0573be0e8 100644 --- a/ramls/tenantItemPair.json +++ b/ramls/tenantItemPair.json @@ -3,14 +3,14 @@ "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" - }, - "tenantId": { - "type": "string", - "description": "Unique ID of the tenant where the item is located" } }, "additionalProperties": false, diff --git a/ramls/tenantItemPairCollection.json b/ramls/tenantItemPairCollection.json index f5d34aef0..390c7063a 100644 --- a/ramls/tenantItemPairCollection.json +++ b/ramls/tenantItemPairCollection.json @@ -5,7 +5,7 @@ "properties": { "tenantItemPairs": { "type": "array", - "description": "Unique ID (UUID) of the item", + "description": "Pairs of tenantId and itemId", "items": { "type": "object", "$ref": "tenantItemPair.json" diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index 400d66f0e..8a151a16e 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -19,7 +19,6 @@ import org.folio.TenantItemPair; import org.folio.TenantItemPairCollection; import org.folio.inventory.common.WebContext; -import org.folio.inventory.storage.Storage; import org.folio.inventory.storage.external.CollectionResourceClient; import org.folio.inventory.support.JsonArrayHelper; import org.folio.inventory.support.http.client.OkapiHttpClient; @@ -62,7 +61,7 @@ public void register(Router router) { private void getItemsFromTenants(RoutingContext routingContext) { var getItemsFutures = routingContext.body().asPojo(TenantItemPairCollection.class) .getTenantItemPairs().stream() - .collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getTenantId, toList()))) + .collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getItemId, toList()))) .entrySet().stream() .map(tenantToItems -> getItemsWithTenantId(tenantToItems.getKey(), tenantToItems.getValue(), routingContext)) .toList(); diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java index 5ad802f62..9c2c39a79 100644 --- a/src/test/java/api/items/TenantItemApiTests.java +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -49,12 +49,12 @@ public void testTenantItemsGetFromDifferentTenants() throws MalformedURLExceptio assertThat(consortiumItem.getString(ID)).matches(consortiumItemId.toString()); assertThat(collegeItem.getString(ID)).matches(collegeItemId.toString()); - var tenantItemPariCollection = constructTenantItemPairCollection(Map.of( + 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(tenantItemPariCollection)) + var response = okapiClient.post(ApiRoot.tenantItems(), JsonObject.mapFrom(tenantItemPairCollection)) .toCompletableFuture().get(5, TimeUnit.SECONDS); assertThat(response.getStatusCode()).isEqualTo(200); @@ -94,10 +94,10 @@ private List extractItems(Response itemsResponse, int expected) { return items; } - private TenantItemPairCollection constructTenantItemPairCollection(Map itemToTenantIds) { + private TenantItemPairCollection constructTenantItemPairCollection(Map tenantsToItemIds) { return new TenantItemPairCollection() - .withTenantItemPairs(itemToTenantIds.entrySet().stream() - .map(pair -> new TenantItemPair().withItemId(pair.getKey()).withTenantId(pair.getValue())) + .withTenantItemPairs(tenantsToItemIds.entrySet().stream() + .map(pair -> new TenantItemPair().withTenantId(pair.getKey()).withItemId(pair.getValue())) .toList()); } From a61fc630b60e85dea3255819dedba6318ed3f28f Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Tue, 27 Aug 2024 17:45:31 +0400 Subject: [PATCH 13/16] [MODINVSTOR-1243] Add tenantId to items --- .../folio/inventory/resources/TenantItems.java | 16 ++++++++++++---- src/test/java/api/items/TenantItemApiTests.java | 3 +++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index 8a151a16e..29115ae41 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -41,6 +41,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 TOTAL_RECORDS_FIELD = "totalRecords"; + public static final String TENANT_ID_FIELD = "tenantId"; private final HttpClient client; @@ -76,7 +77,7 @@ private void getItemsFromTenants(RoutingContext routingContext) { } private CompletableFuture> getItemsWithTenantId(String tenantId, List itemIds, RoutingContext routingContext) { - log.info("getItemsWithTenantId:: Fetching items - [{}] from tenant - {}", itemIds, tenantId); + log.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId); var context = new WebContext(routingContext); CollectionResourceClient itemsStorageClient; try { @@ -93,9 +94,16 @@ private CompletableFuture> getItemsWithTenantId(String tenantId itemsStorageClient.getAll(getByIdsQuery, itemsFetched::complete); return itemsFetched.thenApplyAsync(response -> - response.getStatusCode() == 200 && response.hasBody() - ? JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD) - : List.of()); + getItemsWithTenantId(tenantId, response)); + } + + private List getItemsWithTenantId(String tenantId, Response response) { + if (response.getStatusCode() == 200 && 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) { diff --git a/src/test/java/api/items/TenantItemApiTests.java b/src/test/java/api/items/TenantItemApiTests.java index 9c2c39a79..4b0095bcd 100644 --- a/src/test/java/api/items/TenantItemApiTests.java +++ b/src/test/java/api/items/TenantItemApiTests.java @@ -7,6 +7,7 @@ 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; @@ -58,6 +59,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); var items = extractItems(response, 2); assertThat(items).contains(consortiumItem, collegeItem); } From 6ae8a4df052cf1ca83a182880fa4ba3687dd68eb Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Tue, 27 Aug 2024 18:10:22 +0400 Subject: [PATCH 14/16] [MODINVSTOR-1243] Fix condition --- src/main/java/org/folio/inventory/resources/TenantItems.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index 29115ae41..f0a190a4e 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -98,7 +98,7 @@ private CompletableFuture> getItemsWithTenantId(String tenantId } private List getItemsWithTenantId(String tenantId, Response response) { - if (response.getStatusCode() == 200 && response.hasBody()) { + if (response.getStatusCode() != 200 || !response.hasBody()) { return List.of(); } return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD).stream() From ddb33ebd86284f6619297cc654c3371596e94cc6 Mon Sep 17 00:00:00 2001 From: Serhii_Nosko Date: Wed, 28 Aug 2024 10:31:57 +0300 Subject: [PATCH 15/16] [MODINV-1070] Use separate permission --- descriptors/ModuleDescriptor-template.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index a1bac0693..5bc3ee80b 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -368,7 +368,7 @@ { "methods": ["POST"], "pathPattern": "/inventory/tenant-items", - "permissionsRequired": ["inventory.items.collection.get"], + "permissionsRequired": ["inventory.tenant-items.collection.get"], "modulePermissions": [ "inventory-storage.items.collection.get" ] @@ -689,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", @@ -836,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", From 638f9d9d1bc54ac4b2f1b4f76801e6c12bd93d72 Mon Sep 17 00:00:00 2001 From: Serhii_Nosko Date: Wed, 28 Aug 2024 18:25:55 +0300 Subject: [PATCH 16/16] [MODINV-1070] Code review --- .../org/folio/inventory/resources/TenantItems.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/inventory/resources/TenantItems.java b/src/main/java/org/folio/inventory/resources/TenantItems.java index f0a190a4e..c6079b6f4 100644 --- a/src/main/java/org/folio/inventory/resources/TenantItems.java +++ b/src/main/java/org/folio/inventory/resources/TenantItems.java @@ -14,6 +14,7 @@ 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; @@ -34,9 +35,13 @@ 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 Logger LOG = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items"; public static final String ITEMS_FIELD = "items"; @@ -77,7 +82,7 @@ private void getItemsFromTenants(RoutingContext routingContext) { } private CompletableFuture> getItemsWithTenantId(String tenantId, List itemIds, RoutingContext routingContext) { - log.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId); + LOG.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId); var context = new WebContext(routingContext); CollectionResourceClient itemsStorageClient; try { @@ -98,7 +103,7 @@ private CompletableFuture> getItemsWithTenantId(String tenantId } private List getItemsWithTenantId(String tenantId, Response response) { - if (response.getStatusCode() != 200 || !response.hasBody()) { + if (response.getStatusCode() != HttpStatus.SC_OK || !response.hasBody()) { return List.of(); } return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD).stream()