From 505c465f420cdaf1e03b70114e1af2e724ba8625 Mon Sep 17 00:00:00 2001 From: viacheslav_kolesnyk Date: Fri, 28 Aug 2026 13:29:59 +0200 Subject: [PATCH] feat(providers): Add access types to eholdings/providers/{id}/packages response - Add field to response - Add optional includes API parameter to include new field in response Implements: MODKBEKBJ-839 --- NEWS.md | 1 + ramls/providers.raml | 9 ++++ .../types/packages/packageCollectionItem.json | 8 ++++ .../PackageCollectionResultConverter.java | 39 +++++++++++++--- .../rest/impl/EholdingsProvidersImpl.java | 44 ++++++++++++++----- .../rmapi/result/PackageCollectionResult.java | 11 ++++- ...EholdingsProvidersImplIntegrationTest.java | 32 ++++++++++++++ .../expected-package-by-id-with-provider.json | 3 +- ...d-package-collection-with-one-element.json | 3 +- ...ected-resource-by-id-with-all-objects.json | 3 +- .../expected-resource-by-id-with-package.json | 3 +- 11 files changed, 133 insertions(+), 23 deletions(-) diff --git a/NEWS.md b/NEWS.md index afe03b047..0aae3eeb7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,7 @@ * Migrate Package Endpoints to HoldingsIQ v2 API ([MODKBEKBJ-804](https://folio-org.atlassian.net/browse/MODKBEKBJ-804)) * Add additional fields to POST /eholdings/packages endpoint ([MODKBEKBJ-823](https://folio-org.atlassian.net/browse/MODKBEKBJ-823)) * Update visibility hidden reason handling ([MODKBEKBJ-823](https://folio-org.atlassian.net/browse/MODKBEKBJ-823)) +* Add access types to eholdings/providers/{id}/packages response ([MODKBEKBJ-839](https://folio-org.atlassian.net/browse/MODKBEKBJ-839)) ### Bug fixes * Fix offset handling when retrieving holdings from HoldingsIQ. ([MODKBEKBJ-825](https://folio-org.atlassian.net/browse/MODKBEKBJ-825)) diff --git a/ramls/providers.raml b/ramls/providers.raml index a81ae4c3b..4f7b4d012 100644 --- a/ramls/providers.raml +++ b/ramls/providers.raml @@ -132,6 +132,15 @@ traits: is: [packageQueriable, taggable, accessible, packageFilterable, sortable: {defaultValue: 'relevance', possibleValues: 'name, relevance'}, pageable: {maxCountValue: 100, defaultCountValue: 25}] + queryParameters: + include: + displayName: Include access type + type: string + description: | + Include related objects in response + Possible values are + - accessType + required: false responses: 200: description: OK diff --git a/ramls/types/packages/packageCollectionItem.json b/ramls/types/packages/packageCollectionItem.json index 1213ec818..9d8c5cf14 100644 --- a/ramls/types/packages/packageCollectionItem.json +++ b/ramls/types/packages/packageCollectionItem.json @@ -25,6 +25,14 @@ "type": "object", "description": "Displays if any resources are included in relationships", "$ref": "packageRelationships.json" + }, + "included": { + "type": "array", + "description": "List of included objects", + "items": { + "type": "object", + "javaType": "java.lang.Object" + } } } } diff --git a/src/main/java/org/folio/rest/converter/packages/PackageCollectionResultConverter.java b/src/main/java/org/folio/rest/converter/packages/PackageCollectionResultConverter.java index 628ede9b3..9b6c89133 100644 --- a/src/main/java/org/folio/rest/converter/packages/PackageCollectionResultConverter.java +++ b/src/main/java/org/folio/rest/converter/packages/PackageCollectionResultConverter.java @@ -4,13 +4,18 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import org.folio.holdingsiq.model.PackageData; import org.folio.holdingsiq.model.PackageId; -import org.folio.holdingsiq.model.Packages; +import org.folio.repository.accesstypes.DbAccessType; import org.folio.repository.packages.DbPackage; +import org.folio.rest.jaxrs.model.AccessType; +import org.folio.rest.jaxrs.model.HasOneRelationship; +import org.folio.rest.jaxrs.model.MetaDataIncluded; import org.folio.rest.jaxrs.model.MetaTotalResults; import org.folio.rest.jaxrs.model.PackageCollection; import org.folio.rest.jaxrs.model.PackageCollectionItem; +import org.folio.rest.jaxrs.model.RelationshipData; import org.folio.rest.jaxrs.model.Tags; import org.folio.rest.util.RestConstants; import org.folio.rmapi.result.PackageCollectionResult; @@ -21,22 +26,27 @@ public class PackageCollectionResultConverter implements Converter { private final Converter packageCollectionItemConverter; + private final Converter accessTypeConverter; public PackageCollectionResultConverter( - Converter packageCollectionItemConverter) { + Converter packageCollectionItemConverter, + Converter accessTypeConverter) { this.packageCollectionItemConverter = packageCollectionItemConverter; + this.accessTypeConverter = accessTypeConverter; } @Override public PackageCollection convert(PackageCollectionResult packagesResult) { - Packages packages = packagesResult.packages(); - List dbPackages = packagesResult.dbPackages(); - List packageList = mapItems(packages.getPackagesList(), + var packages = packagesResult.packages(); + var dbPackages = packagesResult.dbPackages(); + var accessTypes = packagesResult.accessTypes(); + var packageList = mapItems(packages.getPackagesList(), packageData -> { - PackageCollectionItem item = packageCollectionItemConverter.convert(packageData); + var item = packageCollectionItemConverter.convert(packageData); item.getAttributes() .withTags(new Tags() .withTagList(getTagsById(dbPackages, createPackageId(packageData)))); + addAccessType(item, packageData.getFullPackageId(), accessTypes); return item; }); @@ -46,6 +56,23 @@ public PackageCollection convert(PackageCollectionResult packagesResult) { .withData(packageList); } + private void addAccessType(PackageCollectionItem item, String packageFullId, + Map accessTypes) { + var dbAccessType = accessTypes.get(packageFullId); + if (dbAccessType == null) { + return; + } + var accessType = accessTypeConverter.convert(dbAccessType); + item.getIncluded().add(accessType); + item.getRelationships() + .withAccessType(new HasOneRelationship() + .withData(new RelationshipData() + .withId(accessType.getId()) + .withType(AccessType.Type.ACCESS_TYPES.value())) + .withMeta(new MetaDataIncluded() + .withIncluded(true))); + } + private List getTagsById(List packages, PackageId packageId) { return packages.stream() .filter(dbPackage -> dbPackage.getId().equals(packageId)) diff --git a/src/main/java/org/folio/rest/impl/EholdingsProvidersImpl.java b/src/main/java/org/folio/rest/impl/EholdingsProvidersImpl.java index b6538ee62..0ef80cb8c 100644 --- a/src/main/java/org/folio/rest/impl/EholdingsProvidersImpl.java +++ b/src/main/java/org/folio/rest/impl/EholdingsProvidersImpl.java @@ -2,6 +2,7 @@ import static java.util.Collections.emptyList; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.folio.common.ListUtils.parseByComma; import static org.folio.db.RowSetUtils.toUUID; import static org.folio.rest.util.ExceptionMappers.error422InputValidationMapper; import static org.folio.rest.util.IdParser.getPackageIds; @@ -14,6 +15,7 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -60,6 +62,7 @@ import org.folio.rest.validator.ProviderTagsPutBodyValidator; import org.folio.rmapi.result.PackageCollectionResult; import org.folio.rmapi.result.VendorResult; +import org.folio.service.accesstypes.AccessTypesService; import org.folio.service.kbcredentials.UserKbCredentialsService; import org.folio.service.loader.FilteredEntitiesLoader; import org.folio.service.loader.RelatedEntitiesLoader; @@ -72,6 +75,7 @@ public class EholdingsProvidersImpl implements EholdingsProviders { private static final String GET_PROVIDER_NOT_FOUND_MESSAGE = "Provider not found"; + private static final String ACCESS_TYPE_INCLUDE_PARAM = "accessType"; @Autowired private Converter putRequestConverter; @@ -88,6 +92,8 @@ public class EholdingsProvidersImpl implements EholdingsProviders { @Autowired private PackageRepository packageRepository; @Autowired + private AccessTypesService accessTypesService; + @Autowired private RelatedEntitiesLoader relatedEntitiesLoader; @Autowired private FilteredEntitiesLoader filteredEntitiesLoader; @@ -197,7 +203,7 @@ tags, new RequestContext(headers).getTenant()) @Override @Validate @HandleValidationErrors - public void getEholdingsProvidersPackagesByProviderId(String providerId, String q, String queryField, + public void getEholdingsProvidersPackagesByProviderId(String providerId, String include, String q, String queryField, String queryType, boolean highlight, List filterTags, List filterAccessType, String filterSelected, String filterType, String filterVisibility, String filterAccess, @@ -222,7 +228,9 @@ public void getEholdingsProvidersPackagesByProviderId(String providerId, String .count(count) .build(); - RmApiTemplate template = templateFactory.createTemplate(okapiHeaders, asyncResultHandler); + var includedObjects = parseByComma(include); + + var template = templateFactory.createTemplate(okapiHeaders, asyncResultHandler); if (filter.isTagsFilter()) { template.requestAction( context -> filteredEntitiesLoader.fetchPackagesByTagFilter(TagFilter.from(filter), context)); @@ -231,7 +239,7 @@ public void getEholdingsProvidersPackagesByProviderId(String providerId, String .fetchPackagesByAccessTypeFilter(AccessTypeFilter.from(filter), context) .thenApply(packages -> new PackageCollectionResult(packages, emptyList()))); } else { - template.requestAction(retrieveFilteredPackages(parsedProviderId, filter)); + template.requestAction(retrieveFilteredPackages(parsedProviderId, filter, includedObjects)); } template .addErrorMapper(ResourceNotFoundException.class, exception -> @@ -241,13 +249,32 @@ public void getEholdingsProvidersPackagesByProviderId(String providerId, String } private Function> retrieveFilteredPackages(int providerId, - PackageRecordFilter filter) { + PackageRecordFilter filter, + List includedObjects) { var packageFilter = filter.toClientFilter(searchProperties); var pageable = filter.toPageable(); return context -> context.getPackagesService() .retrievePackages(providerId, packageFilter, pageable) - .thenCompose(packages -> loadTags(packages, context)); + .thenCompose(packages -> loadTagsAndAccessTypes(packages, includedObjects, context)); + } + + private CompletableFuture loadTagsAndAccessTypes(Packages packages, + List includedObjects, + RmApiTemplateContext context) { + var credentialsId = toUUID(context.getCredentialsId()); + var tenant = context.getRequestContext().getTenant(); + var tagsFuture = packageRepository.findByIds(getPackageIds(packages), credentialsId, tenant); + if (includedObjects.contains(ACCESS_TYPE_INCLUDE_PARAM)) { + var packageIds = packages.getPackagesList().stream() + .map(packageData -> packageData.getVendorId() + "-" + packageData.getPackageId()) + .toList(); + var accessTypesFuture = accessTypesService.findPerRecord(context.getCredentialsId(), new ArrayList<>(packageIds), + RecordType.PACKAGE, tenant); + return tagsFuture.thenCombine(accessTypesFuture, + (dbPackages, accessTypes) -> new PackageCollectionResult(packages, dbPackages, accessTypes)); + } + return tagsFuture.thenApply(dbPackages -> new PackageCollectionResult(packages, dbPackages)); } private CompletableFuture loadTags(VendorResult result, RmApiTemplateContext context) { @@ -258,13 +285,6 @@ private CompletableFuture loadTags(VendorResult result, RmApiTempl return relatedEntitiesLoader.loadTags(result, recordKey, context).thenApply(v -> result); } - private CompletableFuture loadTags(Packages packages, RmApiTemplateContext context) { - UUID credentialsId = toUUID(context.getCredentialsId()); - String tenant = context.getRequestContext().getTenant(); - return packageRepository.findByIds(getPackageIds(packages), credentialsId, tenant) - .thenApply(dbPackages -> new PackageCollectionResult(packages, dbPackages)); - } - private CompletableFuture updateTags(DbProvider provider, Tags tags, String tenant) { if (Objects.isNull(tags)) { return completedFuture(null); diff --git a/src/main/java/org/folio/rmapi/result/PackageCollectionResult.java b/src/main/java/org/folio/rmapi/result/PackageCollectionResult.java index d092cc123..da0ac81f7 100644 --- a/src/main/java/org/folio/rmapi/result/PackageCollectionResult.java +++ b/src/main/java/org/folio/rmapi/result/PackageCollectionResult.java @@ -1,7 +1,16 @@ package org.folio.rmapi.result; +import java.util.Collections; import java.util.List; +import java.util.Map; import org.folio.holdingsiq.model.Packages; +import org.folio.repository.accesstypes.DbAccessType; import org.folio.repository.packages.DbPackage; -public record PackageCollectionResult(Packages packages, List dbPackages) { } +public record PackageCollectionResult(Packages packages, List dbPackages, + Map accessTypes) { + + public PackageCollectionResult(Packages packages, List dbPackages) { + this(packages, dbPackages, Collections.emptyMap()); + } +} diff --git a/src/test/java/org/folio/rest/impl/EholdingsProvidersImplIntegrationTest.java b/src/test/java/org/folio/rest/impl/EholdingsProvidersImplIntegrationTest.java index af3f22767..20de3152e 100644 --- a/src/test/java/org/folio/rest/impl/EholdingsProvidersImplIntegrationTest.java +++ b/src/test/java/org/folio/rest/impl/EholdingsProvidersImplIntegrationTest.java @@ -38,7 +38,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.github.tomakehurst.wiremock.matching.RegexPattern; @@ -467,6 +469,36 @@ void shouldReturn404WhenNonProviderIdNotFound() { assertErrorContainsTitle(error, "Provider not found"); } + @Test + void shouldReturnProviderPackagesWithAccessTypeWhenIncludeParamPassed() { + var accessTypes = insertAccessTypes(testData(configuration.getId()), vertx); + var expectedAccessTypeId = accessTypes.getFirst().getId(); + insertAccessTypeMapping(FULL_PACKAGE_ID, PACKAGE, expectedAccessTypeId, vertx); + + mockGet(new RegexPattern(providerPackagesRmApi(STUB_VENDOR_ID) + ".*"), readFile(STUB_PACKAGE_RESPONSE)); + + var collection = getWithOk(PROVIDER_PACKAGES + "?include=accessType").as(PackageCollection.class); + var packageItem = collection.getData().getFirst(); + + assertFalse(packageItem.getIncluded().isEmpty()); + assertEquals(expectedAccessTypeId, + packageItem.getRelationships().getAccessType().getData().getId()); + } + + @Test + void shouldReturnProviderPackagesWithoutAccessTypeWhenIncludeParamNotPassed() { + var accessTypes = insertAccessTypes(testData(configuration.getId()), vertx); + insertAccessTypeMapping(FULL_PACKAGE_ID, PACKAGE, accessTypes.getFirst().getId(), vertx); + + mockGet(new RegexPattern(providerPackagesRmApi(STUB_VENDOR_ID) + ".*"), readFile(STUB_PACKAGE_RESPONSE)); + + var collection = getWithOk(PROVIDER_PACKAGES).as(PackageCollection.class); + var packageItem = collection.getData().getFirst(); + + assertTrue(packageItem.getIncluded().isEmpty()); + assertNull(packageItem.getRelationships().getAccessType()); + } + private String getPackageResponse(String packageName, int packageId, int providerId) { PackageData packageData = readJsonFile(STUB_PACKAGE_JSON_PATH, PackageData.class); return Json.encode(packageData.toBuilder() diff --git a/src/test/resources/responses/kb-ebsco/packages/expected-package-by-id-with-provider.json b/src/test/resources/responses/kb-ebsco/packages/expected-package-by-id-with-provider.json index de2576c89..371fe06af 100644 --- a/src/test/resources/responses/kb-ebsco/packages/expected-package-by-id-with-provider.json +++ b/src/test/resources/responses/kb-ebsco/packages/expected-package-by-id-with-provider.json @@ -52,7 +52,8 @@ "id": "19" } } - } + }, + "included": [] }, "included": [ { diff --git a/src/test/resources/responses/kb-ebsco/packages/expected-package-collection-with-one-element.json b/src/test/resources/responses/kb-ebsco/packages/expected-package-collection-with-one-element.json index 9a17870c7..0c9c6fd6d 100644 --- a/src/test/resources/responses/kb-ebsco/packages/expected-package-collection-with-one-element.json +++ b/src/test/resources/responses/kb-ebsco/packages/expected-package-collection-with-one-element.json @@ -38,7 +38,8 @@ "included": false } } - } + }, + "included": [] } ], "meta": { diff --git a/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-all-objects.json b/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-all-objects.json index 87633804e..ae19510ca 100644 --- a/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-all-objects.json +++ b/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-all-objects.json @@ -225,7 +225,8 @@ "included": false } } - } + }, + "included": [] } ], "jsonapi": { diff --git a/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-package.json b/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-package.json index c7a05bbaa..1bb254e70 100644 --- a/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-package.json +++ b/src/test/resources/responses/kb-ebsco/resources/expected-resource-by-id-with-package.json @@ -148,7 +148,8 @@ "included": false } } - } + }, + "included": [] } ], "jsonapi": {