Skip to content

Commit

Permalink
[MODINVSTOR-1243] Implement endpoint to retrieve items from multiple …
Browse files Browse the repository at this point in the history
…tenants
  • Loading branch information
Saba-Zedginidze-EPAM committed Aug 23, 2024
1 parent 05423ff commit 6dc9293
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 2 deletions.
8 changes: 8 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
10 changes: 10 additions & 0 deletions ramls/inventory.raml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions ramls/tenantItemPair.json
Original file line number Diff line number Diff line change
@@ -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"]
}
17 changes: 17 additions & 0 deletions ramls/tenantItemPairCollection.json
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 2 additions & 0 deletions src/main/java/org/folio/inventory/InventoryVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -71,6 +72,7 @@ public void start(Promise<Void> started) {
new InventoryConfigApi().register(router);
new TenantApi().register(router);
new UpdateOwnershipApi(storage, client, consortiumService).register(router);
new TenantItems(storage, client).register(router);

Handler<AsyncResult<HttpServer>> onHttpServerStart = result -> {
if (result.succeeded()) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/folio/inventory/resources/Items.java
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ private OkapiHttpClient createHttpClient(
exception.toString())));
}

private CollectionResourceClient createItemsStorageClient(
protected CollectionResourceClient createItemsStorageClient(
OkapiHttpClient client,
WebContext context)
throws MalformedURLException {
Expand Down Expand Up @@ -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()));
}
Expand Down
117 changes: 117 additions & 0 deletions src/main/java/org/folio/inventory/resources/TenantItems.java
Original file line number Diff line number Diff line change
@@ -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<List<JsonObject>> getItemsWithTenantId(String tenantId, List<String> 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<Response>();
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<JsonObject> 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())));
}

}

0 comments on commit 6dc9293

Please sign in to comment.