Skip to content

Commit

Permalink
[MODFQMMGR-376] Use custom field UUIDs for BE custom field names
Browse files Browse the repository at this point in the history
  • Loading branch information
bvsharp committed Jan 23, 2025
1 parent 84c95b2 commit 661c90f
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class MigrationConfiguration {
public static final String VERSION_KEY = "_version";
public static final UUID REMOVED_ENTITY_TYPE_ID = UUID.fromString("deadbeef-dead-dead-dead-deaddeadbeef");

private static final String CURRENT_VERSION = "13";
private static final String CURRENT_VERSION = "14";
// TODO: replace this with current version in the future?
private static final String DEFAULT_VERSION = "0";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.folio.fqm.migration.strategies.V10OrganizationStatusValueChange;
import org.folio.fqm.migration.strategies.V11OrganizationNameCodeOperatorChange;
import org.folio.fqm.migration.strategies.V12PurchaseOrderIdFieldRemoval;
import org.folio.fqm.migration.strategies.V13CustomFieldRename;
import org.folio.fqm.migration.strategies.V1ModeOfIssuanceConsolidation;
import org.folio.fqm.migration.strategies.V2ResourceTypeConsolidation;
import org.folio.fqm.migration.strategies.V3RamsonsFieldCleanup;
Expand All @@ -20,6 +21,8 @@
import org.folio.fqm.migration.strategies.V7PatronGroupsValueChange;
import org.folio.fqm.migration.strategies.V8LocationValueChange;
import org.folio.fqm.migration.strategies.V9LocLibraryValueChange;
import org.folio.spring.FolioExecutionContext;
import org.jooq.DSLContext;
import org.springframework.stereotype.Component;

@Component
Expand All @@ -33,7 +36,9 @@ public MigrationStrategyRepository(
LocationUnitsClient locationUnitsClient,
ModesOfIssuanceClient modesOfIssuanceClient,
OrganizationsClient organizationsClient,
PatronGroupsClient patronGroupsClient
PatronGroupsClient patronGroupsClient,
DSLContext jooqContext,
FolioExecutionContext executionContext
) {
this.migrationStrategies =
List.of(
Expand All @@ -49,7 +54,8 @@ public MigrationStrategyRepository(
new V9LocLibraryValueChange(locationUnitsClient),
new V10OrganizationStatusValueChange(),
new V11OrganizationNameCodeOperatorChange(organizationsClient),
new V12PurchaseOrderIdFieldRemoval()
new V12PurchaseOrderIdFieldRemoval(),
new V13CustomFieldRename(executionContext, jooqContext)
// adding a strategy? be sure to update the `CURRENT_VERSION` in MigrationConfiguration!
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.folio.fqm.migration.strategies;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.folio.fql.service.FqlService;
import org.folio.fqm.migration.MigratableQueryInformation;
import org.folio.fqm.migration.MigrationStrategy;
import org.folio.fqm.migration.MigrationUtils;
import org.folio.fqm.migration.warnings.Warning;
import org.folio.spring.FolioExecutionContext;
import org.jooq.DSLContext;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import static org.folio.fqm.repository.EntityTypeRepository.CUSTOM_FIELD_NAME;
import static org.folio.fqm.repository.EntityTypeRepository.CUSTOM_FIELD_PREPENDER;
import static org.folio.fqm.repository.EntityTypeRepository.CUSTOM_FIELD_TYPE;
import static org.folio.fqm.repository.EntityTypeRepository.SUPPORTED_CUSTOM_FIELD_TYPES;
import static org.jooq.impl.DSL.field;

/**
* Version 13 -> 14, handles custom field renaming.
*
* The custom field naming scheme was changed in MODFQMMGR-376. This migration handles updating custom field names to match the new scheme.
*
* @see https://folio-org.atlassian.net/browse/MODFQMMGR-642 for the addition of this migration
*/
@Log4j2
@RequiredArgsConstructor
public class V13CustomFieldRename implements MigrationStrategy {

public static final String SOURCE_VERSION = "13";
public static final String TARGET_VERSION = "14";

static final UUID USERS_ENTITY_TYPE_ID = UUID.fromString("ddc93926-d15a-4a45-9d9c-93eadc3d9bbf");
static final String CUSTOM_FIELD_SOURCE_VIEW = "src_user_custom_fields"; // Only user entity type currently supports custom fields

private final FolioExecutionContext executionContext;
private final DSLContext jooqContext;

private final Map<String, List<Pair<String, String>>> tenantCustomFieldNamePairs = new ConcurrentHashMap<>();

@Override
public String getLabel() {
return "V13 -> V14 Custom field renaming (MODFQMMGR-642)";
}

@Override
public boolean applies(String version) {
return SOURCE_VERSION.equals(version);
}

@Override
public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryInformation query) {
List<Warning> warnings = new ArrayList<>(query.warnings());

return query
.withFqlQuery(
MigrationUtils.migrateFql(
query.fqlQuery(),
originalVersion -> TARGET_VERSION,
(result, key, value) -> {
if (!USERS_ENTITY_TYPE_ID.equals(query.entityTypeId())) {
result.set(key, value); // no-op
return;
}

Optional<Pair<String, String>> namePair = getNamePairs(executionContext.getTenantId())
.stream()
.filter(pair -> pair.getLeft().equals(key))
.findFirst();
String resultKey = namePair.map(Pair::getRight).orElse(key);
result.set(resultKey, value);
}
)
)
.withFields(query
.fields()
.stream()
.map(oldName -> getNamePairs(executionContext.getTenantId())
.stream()
.filter(pair -> pair.getLeft().equals(oldName))
.map(Pair::getRight)
.findFirst()
.orElse(oldName)
).toList()
)
.withWarnings(warnings);
}

private synchronized List<Pair<String, String>> getNamePairs(String tenantId) {
return tenantCustomFieldNamePairs.computeIfAbsent(tenantId, id -> {
try {
return jooqContext
.select(field("id"), field(CUSTOM_FIELD_NAME))
.from(CUSTOM_FIELD_SOURCE_VIEW)
.where(field(CUSTOM_FIELD_TYPE).in(SUPPORTED_CUSTOM_FIELD_TYPES))
.fetch()
.stream()
.map(row -> {
String name = "";
try {
String idValue = row.get("id", String.class);
name = row.get(CUSTOM_FIELD_NAME, String.class);
return Pair.of(name, CUSTOM_FIELD_PREPENDER + idValue);
} catch (Exception e) {
log.error("Error processing custom field {} for tenant ID: {}", name, tenantId, e);
return null;
}
})
.filter(Objects::nonNull)
.toList();
} catch (Exception e) {
log.error("Failed to fetch custom fields for tenant ID: {}", tenantId, e);
return List.of();
}
});
}
}
31 changes: 18 additions & 13 deletions src/main/java/org/folio/fqm/repository/EntityTypeRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.jooq.InsertValuesStep2;
import org.jooq.JSONB;
import org.jooq.Record;
import org.jooq.Record4;
import org.jooq.Record5;
import org.jooq.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -45,6 +45,7 @@ public class EntityTypeRepository {
public static final String ID_FIELD_NAME = "id";
public static final String DEFINITION_FIELD_NAME = "definition";
public static final String STRING_EXTRACTOR = "%s ->> '%s'";
public static final String CUSTOM_FIELD_PREPENDER = "_custom_field_";
public static final String CUSTOM_FIELD_NAME = "jsonb ->> 'name'";
public static final String CUSTOM_FIELD_REF_ID = "jsonb ->> 'refId'";
public static final String CUSTOM_FIELD_TYPE = "jsonb ->> 'type'";
Expand All @@ -70,7 +71,7 @@ FROM jsonb_array_elements(
new ValueWithLabel().label("True").value("true"),
new ValueWithLabel().label("False").value("false")
);
private static final List<String> SUPPORTED_CUSTOM_FIELD_TYPES = List.of(
public static final List<String> SUPPORTED_CUSTOM_FIELD_TYPES = List.of(
CUSTOM_FIELD_TYPE_SINGLE_CHECKBOX,
CUSTOM_FIELD_TYPE_SINGLE_SELECT_DROPDOWN,
CUSTOM_FIELD_TYPE_RADIO_BUTTON,
Expand Down Expand Up @@ -167,8 +168,8 @@ private List<EntityTypeColumn> fetchColumnNamesForCustomFields(String entityType
String sourceViewName = entityTypeDefinition.getSourceView();
String sourceViewExtractor = entityTypeDefinition.getSourceViewExtractor();

Result<Record4<Object, Object, Object, Object>> results = readerJooqContext
.select(field(CUSTOM_FIELD_NAME), field(CUSTOM_FIELD_REF_ID), field(CUSTOM_FIELD_TYPE), field(CUSTOM_FIELD_FILTER_VALUE_GETTER))
Result<Record5<Object, Object, Object, Object, Object>> results = readerJooqContext
.select(field("id"), field(CUSTOM_FIELD_NAME), field(CUSTOM_FIELD_REF_ID), field(CUSTOM_FIELD_TYPE), field(CUSTOM_FIELD_FILTER_VALUE_GETTER))
.from(sourceViewName)
.where(field(CUSTOM_FIELD_TYPE).in(SUPPORTED_CUSTOM_FIELD_TYPES))
.fetch();
Expand All @@ -177,18 +178,19 @@ private List<EntityTypeColumn> fetchColumnNamesForCustomFields(String entityType
.map(row -> {
String name = "";
try {
String id = row.get("id", String.class);
name = row.get(CUSTOM_FIELD_NAME, String.class);
String refId = row.get(CUSTOM_FIELD_REF_ID, String.class);
String type = row.get(CUSTOM_FIELD_TYPE, String.class);
String customFieldValueJson = row.get(CUSTOM_FIELD_FILTER_VALUE_GETTER, String.class);

if (CUSTOM_FIELD_TYPE_SINGLE_SELECT_DROPDOWN.equals(type) || CUSTOM_FIELD_TYPE_RADIO_BUTTON.equals(type)) {
List<ValueWithLabel> columnValues = parseCustomFieldValues(customFieldValueJson);
return handleSingleSelectCustomField(name, refId, sourceViewName, sourceViewExtractor, columnValues);
return handleSingleSelectCustomField(id, name, refId, sourceViewName, sourceViewExtractor, columnValues);
} else if (CUSTOM_FIELD_TYPE_SINGLE_CHECKBOX.equals(type)) {
return handleBooleanCustomField(name, refId, sourceViewExtractor);
return handleBooleanCustomField(id, name, refId, sourceViewExtractor);
} else if (CUSTOM_FIELD_TYPE_TEXTBOX_SHORT.equals(type) || CUSTOM_FIELD_TYPE_TEXTBOX_LONG.equals(type)) {
return handleTextboxCustomField(name, refId, sourceViewExtractor);
return handleTextboxCustomField(id, name, refId, sourceViewExtractor);
}
return null;
} catch (Exception e) {
Expand All @@ -213,11 +215,12 @@ private List<ValueWithLabel> parseCustomFieldValues(String customFieldValueJson)
.toList();
}

private EntityTypeColumn handleBooleanCustomField(String name,
private EntityTypeColumn handleBooleanCustomField(String id,
String name,
String refId,
String sourceViewExtractor) {
return new EntityTypeColumn()
.name(name)
.name(CUSTOM_FIELD_PREPENDER + id)
.dataType(new BooleanType().dataType("booleanType"))
.values(CUSTOM_FIELD_BOOLEAN_VALUES)
.visibleByDefault(false)
Expand All @@ -227,7 +230,8 @@ private EntityTypeColumn handleBooleanCustomField(String name,
.isCustomField(true);
}

private EntityTypeColumn handleSingleSelectCustomField(String name,
private EntityTypeColumn handleSingleSelectCustomField(String id,
String name,
String refId,
String sourceViewName,
String sourceViewExtractor,
Expand All @@ -242,7 +246,7 @@ private EntityTypeColumn handleSingleSelectCustomField(String name,
refId
);
return new EntityTypeColumn()
.name(name)
.name(CUSTOM_FIELD_PREPENDER + id)
.dataType(new StringType().dataType("stringType"))
.values(columnValues)
.visibleByDefault(false)
Expand All @@ -253,11 +257,12 @@ private EntityTypeColumn handleSingleSelectCustomField(String name,
.isCustomField(true);
}

private EntityTypeColumn handleTextboxCustomField(String name,
private EntityTypeColumn handleTextboxCustomField(String id,
String name,
String refId,
String sourceViewExtractor) {
return new EntityTypeColumn()
.name(name)
.name(CUSTOM_FIELD_PREPENDER + id)
.dataType(new StringType().dataType("stringType"))
.visibleByDefault(false)
.valueGetter(String.format(STRING_EXTRACTOR, sourceViewExtractor, refId))
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/folio/fqm/IntegrationTestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private static void postTenant(String body) {
private static void createDummyViews() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
// Create a src_user_custom_fields view, since the Users entity type depends on it for custom fields. Without this, the smoke test will fail.
jdbcTemplate.execute("CREATE VIEW src_user_custom_fields AS SELECT '{}'::jsonb AS jsonb LIMIT 1");
jdbcTemplate.execute("CREATE VIEW src_user_custom_fields AS SELECT NULL::uuid AS id, '{}'::jsonb AS jsonb LIMIT 1");
}

private static void smokeTest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class MigrationStrategyRepositoryTest {
null,
null,
null,
null,
null,
null
);
MigrationService migrationService = new MigrationService(
Expand Down
Loading

0 comments on commit 661c90f

Please sign in to comment.