From a93ba8fac3c3ba9b117cb47cb6a2861c1075ce90 Mon Sep 17 00:00:00 2001 From: psmagin Date: Tue, 3 Dec 2024 22:11:03 +0200 Subject: [PATCH] feat(cn-browse): Implement Indexing and Re-indexing Mechanisms for Call-Numbers Closes: MSEARCH-864 --- .../entity/InstanceCallNumberEntityAgg.java | 17 +++ .../model/index/CallNumberResource.java | 16 +++ .../model/index/InstanceSubResource.java | 1 + .../search/model/types/ReindexEntityType.java | 2 +- .../search/model/types/ResourceType.java | 1 + .../folio/search/service/ResourceService.java | 2 +- .../impl/CallNumberResourceExtractor.java | 133 +++++++++++++++--- .../service/reindex/ReindexConstants.java | 3 +- .../reindex/jdbc/CallNumberRepository.java | 127 ++++++++++++++++- ...CallNumberShelvingOrderFieldProcessor.java | 18 +++ ...CallNumberShelvingOrderFieldProcessor.java | 13 ++ ...CallNumberShelvingOrderFieldProcessor.java | 12 ++ ...CallNumberShelvingOrderFieldProcessor.java | 13 ++ ...CallNumberShelvingOrderFieldProcessor.java | 12 ++ ...CallNumberShelvingOrderFieldProcessor.java | 12 ++ ...sificationShelvingOrderFieldProcessor.java | 17 +-- ...sificationShelvingOrderFieldProcessor.java | 1 + .../shelving/ShelvingOrderFieldProcessor.java | 24 ++++ .../folio/search/utils/CallNumberUtils.java | 16 ++- .../resources/model/instance_call_number.json | 96 +++++++++++++ .../schemas/request/reindexUploadDto.yaml | 2 +- .../reindex/jdbc/CallNumberRepositoryIT.java | 123 ++++++++++++++++ ...cationShelvingOrderFieldProcessorTest.java | 16 +-- .../resources/sql/populate-call-numbers.sql | 28 ++++ 24 files changed, 651 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/folio/search/model/entity/InstanceCallNumberEntityAgg.java create mode 100644 src/main/java/org/folio/search/model/index/CallNumberResource.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/CallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/DefaultCallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/DeweyCallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/LcCallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/NlmCallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/SudocCallNumberShelvingOrderFieldProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/common/shelving/ShelvingOrderFieldProcessor.java create mode 100644 src/main/resources/model/instance_call_number.json create mode 100644 src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java create mode 100644 src/test/resources/sql/populate-call-numbers.sql diff --git a/src/main/java/org/folio/search/model/entity/InstanceCallNumberEntityAgg.java b/src/main/java/org/folio/search/model/entity/InstanceCallNumberEntityAgg.java new file mode 100644 index 000000000..cd784edc6 --- /dev/null +++ b/src/main/java/org/folio/search/model/entity/InstanceCallNumberEntityAgg.java @@ -0,0 +1,17 @@ +package org.folio.search.model.entity; + +import java.util.Set; +import org.folio.search.model.index.InstanceSubResource; + +public record InstanceCallNumberEntityAgg( + String id, + String fullCallNumber, + String callNumber, + String callNumberPrefix, + String callNumberSuffix, + String callNumberTypeId, + String volume, + String enumeration, + String chronology, + String copyNumber, + Set instances) { } diff --git a/src/main/java/org/folio/search/model/index/CallNumberResource.java b/src/main/java/org/folio/search/model/index/CallNumberResource.java new file mode 100644 index 000000000..3345c273f --- /dev/null +++ b/src/main/java/org/folio/search/model/index/CallNumberResource.java @@ -0,0 +1,16 @@ +package org.folio.search.model.index; + +import java.util.Set; + +public record CallNumberResource( + String id, + String fullCallNumber, + String callNumber, + String callNumberPrefix, + String callNumberSuffix, + String callNumberTypeId, + String volume, + String enumeration, + String chronology, + String copyNumber, + Set instances) { } diff --git a/src/main/java/org/folio/search/model/index/InstanceSubResource.java b/src/main/java/org/folio/search/model/index/InstanceSubResource.java index 3fafa8792..44e99fe59 100644 --- a/src/main/java/org/folio/search/model/index/InstanceSubResource.java +++ b/src/main/java/org/folio/search/model/index/InstanceSubResource.java @@ -11,4 +11,5 @@ public class InstanceSubResource { private Boolean shared; private int count; private List typeId; + private List locationId; } diff --git a/src/main/java/org/folio/search/model/types/ReindexEntityType.java b/src/main/java/org/folio/search/model/types/ReindexEntityType.java index 3e2681fbe..8536bc9ea 100644 --- a/src/main/java/org/folio/search/model/types/ReindexEntityType.java +++ b/src/main/java/org/folio/search/model/types/ReindexEntityType.java @@ -12,7 +12,7 @@ public enum ReindexEntityType { SUBJECT("subject", false, true), CONTRIBUTOR("contributor", false, true), CLASSIFICATION("classification", false, true), - CALL_NUMBER("call-number", false, true), + CALL_NUMBER("call_number", false, true), ITEM("item", true, false), HOLDINGS("holdings", true, false); diff --git a/src/main/java/org/folio/search/model/types/ResourceType.java b/src/main/java/org/folio/search/model/types/ResourceType.java index e9ff0a931..3e8f5d09e 100644 --- a/src/main/java/org/folio/search/model/types/ResourceType.java +++ b/src/main/java/org/folio/search/model/types/ResourceType.java @@ -15,6 +15,7 @@ public enum ResourceType { INSTANCE("instance"), INSTANCE_CONTRIBUTOR("contributor"), INSTANCE_CLASSIFICATION("instance_classification"), + INSTANCE_CALL_NUMBER("instance_call_number"), INSTANCE_SUBJECT("instance_subject"), INSTITUTION("institution"), ITEM("item"), diff --git a/src/main/java/org/folio/search/service/ResourceService.java b/src/main/java/org/folio/search/service/ResourceService.java index c1b40ac67..1124d54fc 100644 --- a/src/main/java/org/folio/search/service/ResourceService.java +++ b/src/main/java/org/folio/search/service/ResourceService.java @@ -115,13 +115,13 @@ public FolioIndexOperationResponse indexInstancesById(List resour private Map> processIndexInstanceEvents(List resourceEvents) { var indexEvents = extractEventsForDataMove(resourceEvents); + preProcessEvents(indexEvents); var fetchedInstances = Optional.ofNullable(consortiumTenantExecutor.execute( () -> resourceFetchService.fetchInstancesByIds(indexEvents))) .orElse(Collections.emptyList()).stream() .filter(Objects::nonNull) .toList(); - preProcessEvents(fetchedInstances); return searchDocumentConverter.convert(fetchedInstances); } diff --git a/src/main/java/org/folio/search/service/converter/preprocessor/extractor/impl/CallNumberResourceExtractor.java b/src/main/java/org/folio/search/service/converter/preprocessor/extractor/impl/CallNumberResourceExtractor.java index 3441d85af..5e166e756 100644 --- a/src/main/java/org/folio/search/service/converter/preprocessor/extractor/impl/CallNumberResourceExtractor.java +++ b/src/main/java/org/folio/search/service/converter/preprocessor/extractor/impl/CallNumberResourceExtractor.java @@ -1,23 +1,33 @@ package org.folio.search.service.converter.preprocessor.extractor.impl; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static org.apache.commons.collections4.MapUtils.getMap; import static org.apache.commons.collections4.MapUtils.getString; +import static org.folio.search.utils.CollectionUtils.subtract; import static org.folio.search.utils.SearchConverterUtils.getNewAsMap; import static org.folio.search.utils.SearchConverterUtils.getOldAsMap; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import lombok.extern.log4j.Log4j2; import org.folio.search.domain.dto.ResourceEvent; +import org.folio.search.domain.dto.ResourceEventType; import org.folio.search.domain.dto.TenantConfiguredFeature; import org.folio.search.model.entity.CallNumberEntity; import org.folio.search.model.entity.InstanceCallNumberEntity; +import org.folio.search.model.entity.InstanceCallNumberEntityAgg; +import org.folio.search.model.index.CallNumberResource; import org.folio.search.model.types.ResourceType; import org.folio.search.service.FeatureConfigService; import org.folio.search.service.converter.preprocessor.extractor.ChildResourceExtractor; import org.folio.search.service.reindex.jdbc.CallNumberRepository; +import org.folio.search.utils.CollectionUtils; import org.folio.search.utils.JsonConverter; import org.springframework.stereotype.Component; @@ -35,35 +45,62 @@ public class CallNumberResourceExtractor extends ChildResourceExtractor { public static final String ENUMERATION_FIELD = "enumeration"; public static final String COPY_NUMBER_FIELD = "copyNumber"; + private final CallNumberRepository repository; private final JsonConverter jsonConverter; private final FeatureConfigService featureConfigService; public CallNumberResourceExtractor(CallNumberRepository repository, JsonConverter jsonConverter, FeatureConfigService featureConfigService) { super(repository); + this.repository = repository; this.jsonConverter = jsonConverter; this.featureConfigService = featureConfigService; } @Override public List prepareEvents(ResourceEvent resource) { - return List.of(); + if (!featureConfigService.isEnabled(TenantConfiguredFeature.BROWSE_CALL_NUMBERS)) { + return Collections.emptyList(); + } + var oldCallNumbers = getCallNumberResources(getOldAsMap(resource)); + var newCallNumbers = getCallNumberResources(getNewAsMap(resource)); + + if (oldCallNumbers.equals(newCallNumbers)) { + return emptyList(); + } + + var tenant = resource.getTenant(); + var callNumbersForCreate = subtract(newCallNumbers, oldCallNumbers); + var callNumbersForDelete = subtract(oldCallNumbers, newCallNumbers); + + var idsForCreate = toIds(callNumbersForCreate); + var idsForDelete = toIds(callNumbersForDelete); + + List idsForFetch = new ArrayList<>(); + idsForFetch.addAll(idsForCreate); + idsForFetch.addAll(idsForDelete); + + var entityAggList = repository.fetchByIds(idsForFetch); + var list = getResourceEventsForDeletion(idsForDelete, entityAggList, tenant); + + var list1 = entityAggList.stream() + .map(entities -> toResourceEvent(entities, tenant)) + .toList(); + return CollectionUtils.mergeSafelyToList(list, list1); } @Override public List prepareEventsOnSharing(ResourceEvent resource) { - return List.of(); + return emptyList(); } @Override - public boolean hasChildResourceChanges(ResourceEvent event) { + public boolean hasChildResourceChanges(ResourceEvent resource) { if (!featureConfigService.isEnabled(TenantConfiguredFeature.BROWSE_CALL_NUMBERS)) { return false; } - var oldAsMap = getOldAsMap(event); - var newAsMap = getNewAsMap(event); - var oldCallNumber = constructEntity(oldAsMap); - var newCallNumber = constructEntity(newAsMap); + var oldCallNumber = toCallNumberEntity(getOldAsMap(resource)); + var newCallNumber = toCallNumberEntity(getNewAsMap(resource)); return !oldCallNumber.equals(newCallNumber); } @@ -93,6 +130,74 @@ protected Map constructEntity(Map entityProperti if (!featureConfigService.isEnabled(TenantConfiguredFeature.BROWSE_CALL_NUMBERS)) { return Collections.emptyMap(); } + return toCallNumberEntity(entityProperties) + .map(jsonConverter::convertToMap) + .orElse(Collections.emptyMap()); + } + + @Override + protected String childrenFieldName() { + return ""; + } + + @Override + protected Set> getChildResources(Map event) { + return Set.of(event); + } + + private Set getCallNumberResources(Map event) { + return toCallNumberEntity(event) + .map(Set::of) + .orElse(emptySet()); + } + + private List toIds(Set subtract) { + return subtract.stream() + .map(CallNumberEntity::getId) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private List getResourceEventsForDeletion(List idsForDelete, + List entityAggList, + String tenant) { + var notFoundEntitiesForDelete = new ArrayList<>(idsForDelete); + var iterator = notFoundEntitiesForDelete.iterator(); + while (iterator.hasNext()) { + var callNumber = iterator.next(); + for (var agg : entityAggList) { + if (agg.id().equals(callNumber)) { + iterator.remove(); + } + } + } + + return notFoundEntitiesForDelete.stream() + .map(callNumberId -> toResourceDeleteEvent(callNumberId, tenant)) + .toList(); + } + + private ResourceEvent toResourceDeleteEvent(String id, String tenant) { + return new ResourceEvent() + .id(id) + .tenant(tenant) + .resourceName(ResourceType.INSTANCE_CALL_NUMBER.getName()) + .type(ResourceEventType.DELETE); + } + + private ResourceEvent toResourceEvent(InstanceCallNumberEntityAgg source, String tenant) { + var id = source.id(); + var resource = new CallNumberResource(id, source.fullCallNumber(), source.callNumber(), + source.callNumberPrefix(), source.callNumberSuffix(), source.callNumberTypeId(), source.volume(), + source.enumeration(), source.chronology(), source.copyNumber(), source.instances()); + return new ResourceEvent() + .id(id) + .tenant(tenant) + .resourceName(ResourceType.INSTANCE_CALL_NUMBER.getName()) + .type(ResourceEventType.UPDATE) + ._new(jsonConverter.convertToMap(resource)); + } + + private Optional toCallNumberEntity(Map entityProperties) { var callNumberComponents = getCallNumberComponents(entityProperties); var callNumber = getString(callNumberComponents, CALL_NUMBER_FIELD); if (callNumber != null) { @@ -106,9 +211,9 @@ protected Map constructEntity(Map entityProperti .enumeration(getString(entityProperties, ENUMERATION_FIELD)) .copyNumber(getString(entityProperties, COPY_NUMBER_FIELD)) .build(); - return jsonConverter.convertToMap(callNumberEntity); + return Optional.of(callNumberEntity); } - return Collections.emptyMap(); + return Optional.empty(); } @SuppressWarnings("unchecked") @@ -117,14 +222,4 @@ private Map getCallNumberComponents(Map entityPr Collections.emptyMap()); } - @Override - protected String childrenFieldName() { - return ""; - } - - @Override - protected Set> getChildResources(Map event) { - return Set.of(event); - } - } diff --git a/src/main/java/org/folio/search/service/reindex/ReindexConstants.java b/src/main/java/org/folio/search/service/reindex/ReindexConstants.java index 8311c190b..f666dcb1e 100644 --- a/src/main/java/org/folio/search/service/reindex/ReindexConstants.java +++ b/src/main/java/org/folio/search/service/reindex/ReindexConstants.java @@ -14,7 +14,8 @@ public final class ReindexConstants { ReindexEntityType.HOLDINGS, ResourceType.HOLDINGS, ReindexEntityType.SUBJECT, ResourceType.INSTANCE_SUBJECT, ReindexEntityType.CLASSIFICATION, ResourceType.INSTANCE_CLASSIFICATION, - ReindexEntityType.CONTRIBUTOR, ResourceType.INSTANCE_CONTRIBUTOR + ReindexEntityType.CONTRIBUTOR, ResourceType.INSTANCE_CONTRIBUTOR, + ReindexEntityType.CALL_NUMBER, ResourceType.INSTANCE_CALL_NUMBER ); public static final String CALL_NUMBER_TABLE = "call_number"; diff --git a/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java b/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java index 1f24e315a..45ccaebf3 100644 --- a/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java +++ b/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java @@ -3,16 +3,25 @@ import static org.apache.commons.collections4.MapUtils.getString; import static org.folio.search.service.reindex.ReindexConstants.CALL_NUMBER_TABLE; import static org.folio.search.service.reindex.ReindexConstants.INSTANCE_CALL_NUMBER_TABLE; +import static org.folio.search.utils.CallNumberUtils.calculateFullCallNumber; import static org.folio.search.utils.JdbcUtils.getFullTableName; +import static org.folio.search.utils.JdbcUtils.getParamPlaceholder; import static org.folio.search.utils.JdbcUtils.getParamPlaceholderForUuid; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import lombok.extern.log4j.Log4j2; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.folio.search.configuration.properties.ReindexConfigurationProperties; import org.folio.search.model.entity.ChildResourceEntityBatch; +import org.folio.search.model.entity.InstanceCallNumberEntityAgg; import org.folio.search.model.types.ReindexEntityType; +import org.folio.search.utils.JdbcUtils; import org.folio.search.utils.JsonConverter; import org.folio.spring.FolioExecutionContext; import org.springframework.dao.DataAccessException; @@ -24,6 +33,27 @@ @Repository public class CallNumberRepository extends UploadRangeRepository implements InstanceChildResourceRepository { + private static final String SELECT_QUERY = """ + SELECT c.*, + json_agg( + json_build_object( + 'count', sub.instance_count, + 'tenantId', sub.tenant_id, + 'locationId', sub.location_ids + ) + ) AS instances + FROM (SELECT ins.call_number_id, + ins.tenant_id, + array_agg(DISTINCT ins.location_id) FILTER (WHERE ins.location_id IS NOT NULL) AS location_ids, + count(DISTINCT ins.instance_id) AS instance_count + FROM %1$s.instance_call_number ins + WHERE %2$s + GROUP BY ins.call_number_id, ins.tenant_id) sub + JOIN %1$s.call_number c ON c.id = sub.call_number_id + WHERE %3$s + GROUP BY c.id; + """; + private static final String DELETE_QUERY = """ DELETE FROM %s @@ -52,12 +82,16 @@ public class CallNumberRepository extends UploadRangeRepository implements Insta instance_id, tenant_id, location_id - ) VALUES (?, ?::uuid, ?::uuid, ?, ?::uuid); + ) VALUES (?, ?::uuid, ?::uuid, ?, ?::uuid) + ON CONFLICT DO NOTHING; """; - protected CallNumberRepository(JdbcTemplate jdbcTemplate, - JsonConverter jsonConverter, - FolioExecutionContext context, + private static final String ID_RANGE_INS_WHERE_CLAUSE = "ins.call_number_id >= ? AND ins.call_number_id <= ?"; + private static final String ID_RANGE_CLAS_WHERE_CLAUSE = "c.id >= ? AND c.id <= ?"; + private static final String IDS_INS_WHERE_CLAUSE = "ins.call_number_id IN (%1$s)"; + private static final String IDS_CLAS_WHERE_CLAUSE = "c.id IN (%1$s)"; + + protected CallNumberRepository(JdbcTemplate jdbcTemplate, JsonConverter jsonConverter, FolioExecutionContext context, ReindexConfigurationProperties reindexConfig) { super(jdbcTemplate, jsonConverter, context, reindexConfig); } @@ -90,6 +124,31 @@ protected Optional subEntityTable() { return Optional.of(INSTANCE_CALL_NUMBER_TABLE); } + public List fetchByIds(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return Collections.emptyList(); + } + var sql = SELECT_QUERY.formatted(JdbcUtils.getSchemaName(context), + IDS_INS_WHERE_CLAUSE.formatted(getParamPlaceholder(ids.size())), + IDS_CLAS_WHERE_CLAUSE.formatted(getParamPlaceholder(ids.size()))); + return jdbcTemplate.query(sql, instanceCallNumberAggRowMapper(), ListUtils.union(ids, ids).toArray()); + } + + @Override + public List> fetchByIdRange(String lower, String upper) { + var sql = getFetchBySql(); + return jdbcTemplate.query(sql, instanceCallNumberAggRowMapper(), lower, upper, lower, upper) + .stream() + .map(jsonConverter::convertToMap) + .toList(); + } + + @Override + protected String getFetchBySql() { + return SELECT_QUERY.formatted(JdbcUtils.getSchemaName(context), ID_RANGE_INS_WHERE_CLAUSE, + ID_RANGE_CLAS_WHERE_CLAUSE); + } + @Override protected RowMapper> rowToMapMapper() { return null; @@ -115,9 +174,8 @@ private void saveResourceEntities(ChildResourceEntityBatch entityBatch) { } catch (DataAccessException e) { log.warn("saveAll::Failed to save entities batch. Starting processing one-by-one", e); for (var entity : entityBatch.resourceEntities()) { - jdbcTemplate.update(callNumberSql, - getId(entity), getCallNumber(entity), getPrefix(entity), getSuffix(entity), getTypeId(entity), - getVolume(entity), getEnumeration(entity), getChronology(entity), getCopyNumber(entity)); + jdbcTemplate.update(callNumberSql, getId(entity), getCallNumber(entity), getPrefix(entity), getSuffix(entity), + getTypeId(entity), getVolume(entity), getEnumeration(entity), getChronology(entity), getCopyNumber(entity)); } } } @@ -144,10 +202,45 @@ private void saveRelationshipEntities(ChildResourceEntityBatch entityBatch) { } } + private RowMapper instanceCallNumberAggRowMapper() { + return (rs, rowNum) -> { + var callNumber = getCallNumber(rs); + var callNumberSuffix = getCallNumberSuffix(rs); + var volume = getVolume(rs); + var enumeration = getEnumeration(rs); + var chronology = getChronology(rs); + var copyNumber = getCopyNumber(rs); + return new InstanceCallNumberEntityAgg(getId(rs), + calculateFullCallNumber(callNumber, volume, enumeration, chronology, copyNumber, callNumberSuffix), + callNumber, getCallNumberPrefix(rs), callNumberSuffix, getCallNumberTypeId(rs), volume, enumeration, chronology, + copyNumber, parseInstanceSubResources(getInstances(rs))); + }; + } + + private String getCallNumberSuffix(ResultSet rs) throws SQLException { + return rs.getString("call_number_suffix"); + } + + private String getCallNumberPrefix(ResultSet rs) throws SQLException { + return rs.getString("call_number_prefix"); + } + + private String getCallNumberTypeId(ResultSet rs) throws SQLException { + return rs.getString("call_number_type_id"); + } + + private String getInstances(ResultSet rs) throws SQLException { + return rs.getString("instances"); + } + private String getCallNumber(Map callNumberComponents) { return getString(callNumberComponents, "callNumber"); } + private String getCallNumber(ResultSet rs) throws SQLException { + return rs.getString("call_number"); + } + private String getCallNumberId(Map callNumberComponents) { return getString(callNumberComponents, "callNumberId"); } @@ -172,22 +265,42 @@ private String getId(Map item) { return getString(item, "id"); } + private String getId(ResultSet rs) throws SQLException { + return rs.getString("id"); + } + private String getCopyNumber(Map item) { return getString(item, "copyNumber"); } + private String getCopyNumber(ResultSet rs) throws SQLException { + return rs.getString("copy_number"); + } + private String getEnumeration(Map item) { return getString(item, "enumeration"); } + private String getEnumeration(ResultSet rs) throws SQLException { + return rs.getString("enumeration"); + } + private String getChronology(Map item) { return getString(item, "chronology"); } + private String getChronology(ResultSet rs) throws SQLException { + return rs.getString("chronology"); + } + private String getVolume(Map item) { return getString(item, "volume"); } + private String getVolume(ResultSet rs) throws SQLException { + return rs.getString("volume"); + } + private String getTypeId(Map callNumberComponents) { return getString(callNumberComponents, "callNumberTypeId"); } diff --git a/src/main/java/org/folio/search/service/setter/callnumber/CallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/CallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..baf6acea7 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/CallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,18 @@ +package org.folio.search.service.setter.callnumber; + +import java.util.function.UnaryOperator; +import org.folio.search.model.index.CallNumberResource; +import org.folio.search.service.setter.common.shelving.ShelvingOrderFieldProcessor; + +public abstract class CallNumberShelvingOrderFieldProcessor + extends ShelvingOrderFieldProcessor { + + protected CallNumberShelvingOrderFieldProcessor(UnaryOperator numberFunction) { + super(numberFunction); + } + + @Override + protected String extractNumber(CallNumberResource callNumberResource) { + return callNumberResource.fullCallNumber(); + } +} diff --git a/src/main/java/org/folio/search/service/setter/callnumber/DefaultCallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/DefaultCallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..f4fe18da6 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/DefaultCallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,13 @@ +package org.folio.search.service.setter.callnumber; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class DefaultCallNumberShelvingOrderFieldProcessor extends CallNumberShelvingOrderFieldProcessor { + + protected DefaultCallNumberShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEFAULT)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/callnumber/DeweyCallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/DeweyCallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..cd2dcc0ba --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/DeweyCallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,12 @@ +package org.folio.search.service.setter.callnumber; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class DeweyCallNumberShelvingOrderFieldProcessor extends CallNumberShelvingOrderFieldProcessor { + protected DeweyCallNumberShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEWEY)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/callnumber/LcCallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/LcCallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..ff6cf6b27 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/LcCallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,13 @@ +package org.folio.search.service.setter.callnumber; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class LcCallNumberShelvingOrderFieldProcessor extends CallNumberShelvingOrderFieldProcessor { + + protected LcCallNumberShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.LC)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/callnumber/NlmCallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/NlmCallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..52870eda2 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/NlmCallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,12 @@ +package org.folio.search.service.setter.callnumber; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class NlmCallNumberShelvingOrderFieldProcessor extends CallNumberShelvingOrderFieldProcessor { + protected NlmCallNumberShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.NLM)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/callnumber/SudocCallNumberShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/SudocCallNumberShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..ebc7afd24 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/SudocCallNumberShelvingOrderFieldProcessor.java @@ -0,0 +1,12 @@ +package org.folio.search.service.setter.callnumber; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class SudocCallNumberShelvingOrderFieldProcessor extends CallNumberShelvingOrderFieldProcessor { + protected SudocCallNumberShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.SUDOC)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java index 0245b5b8f..48dd9edb3 100644 --- a/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java +++ b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java @@ -1,23 +1,18 @@ package org.folio.search.service.setter.classification; import java.util.function.UnaryOperator; -import lombok.NonNull; import org.folio.search.model.index.ClassificationResource; -import org.folio.search.service.setter.FieldProcessor; +import org.folio.search.service.setter.common.shelving.ShelvingOrderFieldProcessor; public abstract class ClassificationShelvingOrderFieldProcessor - implements FieldProcessor { + extends ShelvingOrderFieldProcessor { - private final UnaryOperator numberFunction; - - protected ClassificationShelvingOrderFieldProcessor(@NonNull UnaryOperator numberFunction) { - this.numberFunction = numberFunction; + protected ClassificationShelvingOrderFieldProcessor(UnaryOperator numberFunction) { + super(numberFunction); } @Override - public String getFieldValue(ClassificationResource eventBody) { - var number = eventBody.number(); - return numberFunction.apply(number); + protected String extractNumber(ClassificationResource classificationResource) { + return classificationResource.number(); } } - diff --git a/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java index 134568a48..b9ca04e18 100644 --- a/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java +++ b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java @@ -6,6 +6,7 @@ @Component public class DeweyClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor { + protected DeweyClassificationShelvingOrderFieldProcessor() { super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEWEY)); } diff --git a/src/main/java/org/folio/search/service/setter/common/shelving/ShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/common/shelving/ShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..92b6ba612 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/common/shelving/ShelvingOrderFieldProcessor.java @@ -0,0 +1,24 @@ +package org.folio.search.service.setter.common.shelving; + +import java.util.function.UnaryOperator; +import lombok.NonNull; +import org.folio.search.service.setter.FieldProcessor; + +public abstract class ShelvingOrderFieldProcessor + implements FieldProcessor { + + private final UnaryOperator numberFunction; + + protected ShelvingOrderFieldProcessor(@NonNull UnaryOperator numberFunction) { + this.numberFunction = numberFunction; + } + + @Override + public String getFieldValue(T eventBody) { + var number = extractNumber(eventBody); + return numberFunction.apply(number); + } + + protected abstract String extractNumber(T eventBody); +} + diff --git a/src/main/java/org/folio/search/utils/CallNumberUtils.java b/src/main/java/org/folio/search/utils/CallNumberUtils.java index 52e538075..2959b9024 100644 --- a/src/main/java/org/folio/search/utils/CallNumberUtils.java +++ b/src/main/java/org/folio/search/utils/CallNumberUtils.java @@ -46,11 +46,9 @@ public class CallNumberUtils { public static String calculateShelvingOrder(Item item) { var callNumberComponents = item.getEffectiveCallNumberComponents(); if (callNumberComponents != null && isNotBlank(callNumberComponents.getCallNumber())) { - var fullCallNumber = Stream.of(callNumberComponents.getCallNumber(), item.getVolume(), item.getEnumeration(), - item.getChronology(), item.getCopyNumber(), callNumberComponents.getSuffix()) - .filter(StringUtils::isNotBlank) - .map(StringUtils::trim) - .collect(joining(" ")); + var fullCallNumber = calculateFullCallNumber(callNumberComponents.getCallNumber(), + item.getVolume(), item.getEnumeration(), item.getChronology(), item.getCopyNumber(), + callNumberComponents.getSuffix()); return getShelfKeyFromCallNumber(fullCallNumber).orElse(null); } @@ -58,6 +56,14 @@ public static String calculateShelvingOrder(Item item) { return null; } + public static String calculateFullCallNumber(String callNumber, String volume, String enumeration, String chronology, + String copyNumber, String suffix) { + return Stream.of(callNumber, volume, enumeration, chronology, copyNumber, suffix) + .filter(StringUtils::isNotBlank) + .map(StringUtils::trim) + .collect(joining(" ")); + } + public static Optional getShelfKeyFromCallNumber(String callNumber) { return Optional.ofNullable(callNumber) .flatMap(cn -> getValidShelfKey(new LCCallNumber(cn)) diff --git a/src/main/resources/model/instance_call_number.json b/src/main/resources/model/instance_call_number.json new file mode 100644 index 000000000..514350730 --- /dev/null +++ b/src/main/resources/model/instance_call_number.json @@ -0,0 +1,96 @@ +{ + "name": "instance_call_number", + "eventBodyJavaClass": "org.folio.search.model.index.CallNumberResource", + "parent": "instance", + "fields": { + "fullCallNumber" : { + "index": "keyword_icu", + "showInResponse": [ "browse" ] + }, + "callNumber": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "callNumberPrefix": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "callNumberSuffix": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "callNumberTypeId": { + "index": "keyword", + "showInResponse": [ "browse" ] + }, + "volume": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "enumeration": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "chronology": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "copyNumber": { + "index": "source", + "showInResponse": [ "browse" ] + }, + "instances": { + "type": "object", + "properties": { + "tenantId": { + "index": "keyword", + "searchTypes": [ "facet", "filter" ] + }, + "shared": { + "index": "bool", + "searchTypes": [ "facet", "filter" ], + "default": false + }, + "locationId": { + "index": "keyword", + "searchTypes": [ "facet", "filter" ] + }, + "count": { + "index": "source" + } + } + } + }, + "searchFields": { + "fullCallNumber": { + "type": "search", + "index": "keyword_icu", + "processor": "defaultCallNumberShelvingOrderFieldProcessor" + }, + "defaultShelvingOrder": { + "type": "search", + "index": "keyword_icu", + "processor": "defaultCallNumberShelvingOrderFieldProcessor" + }, + "lcShelvingOrder": { + "type": "search", + "index": "keyword_icu", + "processor": "lcCallNumberShelvingOrderFieldProcessor" + }, + "deweyShelvingOrder": { + "type": "search", + "index": "keyword_icu", + "processor": "deweyCallNumberShelvingOrderFieldProcessor" + }, + "nlmShelvingOrder": { + "type": "search", + "index": "keyword_icu", + "processor": "nlmCallNumberShelvingOrderFieldProcessor" + }, + "sudocShelvingOrder": { + "type": "search", + "index": "keyword_icu", + "processor": "sudocCallNumberShelvingOrderFieldProcessor" + } + } +} diff --git a/src/main/resources/swagger.api/schemas/request/reindexUploadDto.yaml b/src/main/resources/swagger.api/schemas/request/reindexUploadDto.yaml index 24c62e83b..f1ef1a3d8 100644 --- a/src/main/resources/swagger.api/schemas/request/reindexUploadDto.yaml +++ b/src/main/resources/swagger.api/schemas/request/reindexUploadDto.yaml @@ -10,9 +10,9 @@ properties: - subject - contributor - classification + - call-number minItems: 1 indexSettings: - description: Index settings to apply for index $ref: '../entity/indexSettings.yaml' required: - entityTypes diff --git a/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java b/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java new file mode 100644 index 000000000..2006e1cf9 --- /dev/null +++ b/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java @@ -0,0 +1,123 @@ +package org.folio.search.service.reindex.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.folio.search.utils.TestUtils.mapOf; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.folio.search.configuration.properties.ReindexConfigurationProperties; +import org.folio.search.model.entity.ChildResourceEntityBatch; +import org.folio.search.utils.JsonConverter; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.FolioModuleMetadata; +import org.folio.spring.testing.extension.EnablePostgres; +import org.folio.spring.testing.extension.impl.RandomParametersExtension; +import org.folio.spring.testing.type.IntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.jdbc.Sql; + +@IntegrationTest +@JdbcTest +@EnablePostgres +@AutoConfigureJson +@ExtendWith(RandomParametersExtension.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class CallNumberRepositoryIT { + + private @SpyBean JdbcTemplate jdbcTemplate; + private @MockBean FolioExecutionContext context; + private CallNumberRepository repository; + private ReindexConfigurationProperties properties; + + @BeforeEach + void setUp() { + properties = new ReindexConfigurationProperties(); + var jsonConverter = new JsonConverter(new ObjectMapper()); + repository = spy(new CallNumberRepository(jdbcTemplate, jsonConverter, context, properties)); + when(context.getFolioModuleMetadata()).thenReturn(new FolioModuleMetadata() { + @Override + public String getModuleName() { + return null; + } + + @Override + public String getDBSchemaName(String tenantId) { + return "public"; + } + }); + when(context.getTenantId()).thenReturn(TENANT_ID); + } + + @Test + @Sql("/sql/populate-call-numbers.sql") + void fetchBy_returnListOfMaps() { + // act + var ranges = repository.fetchByIdRange("cn7", "cna"); + + // assert + assertThat(ranges) + .hasSize(3) + .allMatch(map -> map.keySet().containsAll(List.of("id", "fullCallNumber", "instances"))) + .extracting("id", "fullCallNumber") + .containsExactlyInAnyOrder( + tuple("cn7", "CN-007 Vol7 Enum7 Copy7"), + tuple("cn8", "CN-008 Chron8 Copy8 Suf8"), + tuple("cna", "CN-010 Vol10 Chron10 Copy10") + ); + } + + @Test + void saveAll() { + var entities = Set.of(callNumberEntity("1"), callNumberEntity("2")); + var entityRelations = List.of( + callNumberRelation("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "1"), + callNumberRelation("b3bae8a9-cfb1-4afe-83d5-2cdae4580e07", "2"), + callNumberRelation("9ec55e4f-6a76-427c-b47b-197046f44a54", "2")); + + repository.saveAll(new ChildResourceEntityBatch(entities, entityRelations)); + + // assert + var ranges = repository.fetchByIdRange("0", "50"); + assertThat(ranges) + .hasSize(2) + .extracting("callNumber", "instances") + .contains( + tuple("number1", + List.of(mapOf("count", 1, "locationId", null, "shared", null, "tenantId", TENANT_ID, "typeId", null))), + tuple("number2", + List.of(mapOf("count", 2, "locationId", null, "shared", null, "tenantId", TENANT_ID, "typeId", null)))); + } + + private Map callNumberEntity(String id) { + return Map.of( + "id", id, + "callNumber", "number" + id, + "volume", "vol" + ); + } + + private Map callNumberRelation(String instanceId, String callNumberId) { + return Map.of( + "instanceId", instanceId, + "itemId", "b3e1db88-67e7-4e62-8824-8bd8fe71af9a", + "callNumberId", callNumberId, + "tenantId", TENANT_ID, + "shared", false + ); + } + +} diff --git a/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java b/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java index b9b460957..e35d29e73 100644 --- a/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java +++ b/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java @@ -25,6 +25,14 @@ class ClassificationShelvingOrderFieldProcessorTest { private static final String OUTPUT = "ResultNum"; private ClassificationResource eventBody; + public static Stream testData() { + return Stream.of( + Arguments.arguments(new DefaultClassificationShelvingOrderFieldProcessor(), DEFAULT), + Arguments.arguments(new DeweyClassificationShelvingOrderFieldProcessor(), DEWEY), + Arguments.arguments(new LcClassificationShelvingOrderFieldProcessor(), LC) + ); + } + @BeforeEach void setUp() { eventBody = mock(ClassificationResource.class); @@ -43,12 +51,4 @@ void testDefaultClassificationShelvingOrderFieldProcessor(ClassificationShelving } } - public static Stream testData() { - return Stream.of( - Arguments.arguments(new DefaultClassificationShelvingOrderFieldProcessor(), DEFAULT), - Arguments.arguments(new DeweyClassificationShelvingOrderFieldProcessor(), DEWEY), - Arguments.arguments(new LcClassificationShelvingOrderFieldProcessor(), LC) - ); - } - } diff --git a/src/test/resources/sql/populate-call-numbers.sql b/src/test/resources/sql/populate-call-numbers.sql new file mode 100644 index 000000000..ead2b646e --- /dev/null +++ b/src/test/resources/sql/populate-call-numbers.sql @@ -0,0 +1,28 @@ +-- Insert data into 'call_number' table with some fields set to NULL +INSERT INTO call_number (id, call_number, call_number_prefix, call_number_suffix, call_number_type_id, volume, enumeration, chronology, copy_number) +VALUES + ('cn1', 'CN-001', 'Pre1', NULL, 'Type1', 'Vol1', 'Enum1', 'Chron1', NULL), + ('cn2', 'CN-002', NULL, 'Suf2', 'Type2', 'Vol2', NULL, 'Chron2', 'Copy2'), + ('cn3', 'CN-003', 'Pre3', 'Suf3', NULL, NULL, 'Enum3', NULL, 'Copy3'), + ('cn4', 'CN-004', NULL, NULL, 'Type4', 'Vol4', 'Enum4', 'Chron4', NULL), + ('cn5', 'CN-005', 'Pre5', 'Suf5', NULL, 'Vol5', NULL, NULL, 'Copy5'), + ('cn6', 'CN-006', NULL, 'Suf6', 'Type6', NULL, 'Enum6', 'Chron6', NULL), + ('cn7', 'CN-007', 'Pre7', NULL, NULL, 'Vol7', 'Enum7', NULL, 'Copy7'), + ('cn8', 'CN-008', NULL, 'Suf8', 'Type8', NULL, NULL, 'Chron8', 'Copy8'), + ('cn9', 'CN-009', 'Pre9', 'Suf9', 'Type9', 'Vol9', 'Enum9', NULL, NULL), + ('cna', 'CN-010', NULL, NULL, NULL, 'Vol10', NULL, 'Chron10', 'Copy10'); + +-- Insert data into 'instance_call_number' table with varied location and count +INSERT INTO instance_call_number (call_number_id, item_id, instance_id, tenant_id, location_id) +VALUES + ('cn1', 'b1703fff-6fdb-45d5-bd9e-d5596658a5c5', '43e1b37b-b3aa-438b-bf3a-2ca4c93ab045', 'tenant1', 'b0a5b238-243b-4f66-afa6-20b605cc58cb'), + ('cn1', 'd13e75e0-632e-4d24-b689-a5c7012ddfd7', '43e1b37b-b3aa-438b-bf3a-2ca4c93ab045', 'tenant2', '51f4b5b6-74a3-431b-97e1-5e3b1c6b6e9a'), + ('cn2', 'e4b506e4-8ec3-4f7b-9e5f-8140370e5498', '6780c3a5-e16c-4a0a-8598-5b0f0a2c5473', 'tenant1', '731d865a-4719-4c34-893b-09f7069f4ace'), + ('cn3', 'f1a14562-c929-4b21-86eb-786f4076b728', '809857a8-2554-4a58-b3bf-350c9b3a2f0e', 'tenant2', 'e89befb2-c947-4b25-b488-d3a1dc0e5032'), + ('cn3', '0739eca6-23d8-490a-afe4-4886aeb04746', '809857a8-2554-4a58-b3bf-350c9b3a2f0e', 'tenant1', '4e2f5e0e-a8df-45d0-a036-ef9e90b1e8f3'), + ('cn4', '4da99244-268f-4027-ad33-507c3213c3b3', '96c561c2-8dfd-4a57-91ae-da831e0daa9e', 'tenant1', '3ec83352-e8fa-4c58-ab7f-12fcdc0a7f70'), + ('cn5', '22d7b090-b4af-49d3-999f-ca7b78b3c565', '1c146d78-5ba9-4878-b639-30c05fe850b8', 'tenant2', '89216ffe-439d-4c88-b07c-3a4da24e6b11'), + ('cn7', '306afa59-3b12-4601-9f34-43f7db3acb5d', '2ad5b301-4b8d-4c7e-a96f-7688b8fee1a9', 'tenant1', '8bc5bf56-d9a0-4e65-83a5-95d190ffb803'), + ('cn8', '7e5eca0f-9a92-4a8e-a792-be9b0dd8f1ee', '63bb3e28-da97-46aa-a5b9-8e7d2180cf6b', 'tenant1', '59583815-e8ae-41d4-ac83-52b8f0a50f61'), + ('cna', '921b3eb4-06f7-41f5-aabc-3c334337a121', '7fb0e775-badc-4916-92e4-e202b340d580', 'tenant1', '9b743643-d7ab-477b-3175-249a50b3191e'), + ('cna', 'bace7c12-f1be-4d6f-a385-4d2075db9a55', '7fb0e775-badc-4916-92e4-e202b340d580', 'tenant1', '17ac3b08-5472-4ea7-a8f8-8820f83368ba'); \ No newline at end of file