From 351b93c5db77f29eacf3788c2fd4336b6b792f59 Mon Sep 17 00:00:00 2001 From: Noah Overcash Date: Fri, 2 Aug 2024 08:58:22 -0400 Subject: [PATCH] [MODFQMMGR-389] Migration service implementation (#336) * [MODFQMMGR-389] Migration service interface but string * Bump query processor to snapshot * Architecture * Migration helper * First two entity types * docblock * staticize * docblocks * adjust template to be a bit less heavy on creation * qol improvements * Remove translations from removed entity types * More qol * completed mappings * oops, those are important * Fix tests * optimize * Basic migration strategy tester * AbstractSimpleMigrationStrategy tests * add tests for all but poc migration logic * Full migration service test * poc migration tests * Initial warnings infrastructure * typo * lombok constructors * Add warnings class implementations tests * abstract strategy coverage goes wheee * dedupe * Test entity type warnings * test new migration warning logic * re-abstract --- PULL_REQUEST_TEMPLATE.md | 3 + entity-type-creator/bun.lockb | Bin 96101 -> 96946 bytes .../components/MigrationHelper.tsx | 279 +++++++++++ entity-type-creator/package.json | 1 + entity-type-creator/pages/index.tsx | 7 +- pom.xml | 2 +- .../AbstractSimpleMigrationStrategy.java | 191 +++++++ .../migration/MigratableQueryInformation.java | 22 + .../fqm/migration/MigrationStrategy.java | 25 + .../MigrationStrategyRepository.java | 19 + .../migration/strategies/V0POCMigration.java | 464 ++++++++++++++++++ .../warnings/DeprecatedEntityWarning.java | 47 ++ .../warnings/DeprecatedFieldWarning.java | 45 ++ .../migration/warnings/EntityTypeWarning.java | 4 + .../fqm/migration/warnings/FieldWarning.java | 4 + .../warnings/QueryBreakingWarning.java | 33 ++ .../warnings/RemovedEntityWarning.java | 33 ++ .../warnings/RemovedFieldWarning.java | 33 ++ .../folio/fqm/migration/warnings/Warning.java | 60 +++ .../fqm/service/LocalizationService.java | 5 +- .../folio/fqm/service/MigrationService.java | 67 ++- src/main/resources/entity-types/removed.json5 | 14 + .../AbstractSimpleMigrationStrategyTest.java | 354 +++++++++++++ .../MigrationStrategyRepositoryTest.java | 105 ++++ .../migration/strategies/TestTemplate.java | 48 ++ .../strategies/V0POCMigrationTest.java | 182 +++++++ .../fqm/migration/warnings/WarningTest.java | 116 +++++ .../repository/ResultSetRepositoryTest.java | 8 +- .../fqm/service/MigrationServiceTest.java | 192 ++++++++ .../service/QueryProcessorServiceTest.java | 6 +- .../org/folio/fqm/utils/IdStreamerTest.java | 2 +- translations/mod-fqm-manager/en.json | 449 +++-------------- 32 files changed, 2405 insertions(+), 415 deletions(-) create mode 100644 entity-type-creator/components/MigrationHelper.tsx create mode 100644 src/main/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategy.java create mode 100644 src/main/java/org/folio/fqm/migration/MigratableQueryInformation.java create mode 100644 src/main/java/org/folio/fqm/migration/MigrationStrategy.java create mode 100644 src/main/java/org/folio/fqm/migration/MigrationStrategyRepository.java create mode 100644 src/main/java/org/folio/fqm/migration/strategies/V0POCMigration.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/DeprecatedEntityWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/DeprecatedFieldWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/EntityTypeWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/FieldWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/QueryBreakingWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/RemovedEntityWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/RemovedFieldWarning.java create mode 100644 src/main/java/org/folio/fqm/migration/warnings/Warning.java create mode 100644 src/main/resources/entity-types/removed.json5 create mode 100644 src/test/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategyTest.java create mode 100644 src/test/java/org/folio/fqm/migration/MigrationStrategyRepositoryTest.java create mode 100644 src/test/java/org/folio/fqm/migration/strategies/TestTemplate.java create mode 100644 src/test/java/org/folio/fqm/migration/strategies/V0POCMigrationTest.java create mode 100644 src/test/java/org/folio/fqm/migration/warnings/WarningTest.java create mode 100644 src/test/java/org/folio/fqm/service/MigrationServiceTest.java diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 6b7e66a6..f32c9f33 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -19,3 +19,6 @@ If you are adding entity type(s), have you: - [ ] Added views to liquibase, as applicable? - [ ] Added required interfaces to the module descriptor? - [ ] Checked that querying fields works correctly and all SQL is valid? + +If you are changing/removing entity type(s), have you: +- [ ] Added migration code for any changes? \ No newline at end of file diff --git a/entity-type-creator/bun.lockb b/entity-type-creator/bun.lockb index 6be0192e9d7c8802b0d412de9b221efdca01a862..3c83885c05d513bdf28e2d1f529bde3228268f70 100755 GIT binary patch delta 13704 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 QZ>2N8!P`rxumaP618fD?g8%>k diff --git a/entity-type-creator/components/MigrationHelper.tsx b/entity-type-creator/components/MigrationHelper.tsx new file mode 100644 index 00000000..63fae216 --- /dev/null +++ b/entity-type-creator/components/MigrationHelper.tsx @@ -0,0 +1,279 @@ +import { EntityType } from '@/types'; +import { java } from '@codemirror/lang-java'; +import { json } from '@codemirror/lang-json'; +import { Button, Container, FormControlLabel, Grid, Switch } from '@mui/material'; +import CodeMirror, { EditorView } from '@uiw/react-codemirror'; +import { constantCase } from 'change-case'; +import json5 from 'json5'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +export default function MigrationHelper() { + const [oldEntityType, setOldEntityType] = useState(null); + const [newEntityType, setNewEntityType] = useState(null); + const [columnMapping, setColumnMapping] = useState>({}); + + const canvasRef = useRef(); + + const [showMatched, setShowMatched] = useState(true); + const [showOnlyMatchingDataTypes, setShowOnlyMatchingDataTypes] = useState(false); + const [clicked, setClicked] = useState(null); + const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); + + const result = useMemo(() => { + let result = ''; + + const oldAndNewAreSame = oldEntityType?.id === newEntityType?.id; + + const oldNameUnqualified = constantCase(oldEntityType?.name ?? 'FillMeIn'); + const oldName = oldAndNewAreSame ? oldNameUnqualified : `OLD_${oldNameUnqualified}`; + const newNameUnqualified = constantCase(newEntityType?.name ?? 'FillMeIn'); + const newName = oldAndNewAreSame ? newNameUnqualified : `NEW_${newNameUnqualified}`; + + if (oldAndNewAreSame) { + result += ` +private static final UUID ${oldName} = UUID.fromString("${oldEntityType?.id}"); +`; + } else { + result += ` +private static final UUID ${oldName} = UUID.fromString("${oldEntityType?.id}"); +private static final UUID ${newName} = UUID.fromString("${newEntityType?.id}"); +`; + } + + const columnMapName = oldNameUnqualified + '_COLUMN_MAPPING'; + result += ` +private static final Map ${columnMapName} = Map.ofEntries( + ${Object.entries(columnMapping) + .toSorted(([a], [b]) => a.localeCompare(b)) + .filter(([a, b]) => a !== b) + .map(([oldName, newName]) => `Map.entry("${oldName}", "${newName}")`) + .join(',\n ')} +); +`.trimStart(); + + if (oldAndNewAreSame) { + result += ` +@Override +protected Map getEntityTypeChanges() { + return Map.ofEntries(); +} +`; + } else { + result += ` +@Override +protected Map getEntityTypeChanges() { + return Map.ofEntries( + Map.entry(${oldName}, ${newName}) + ); +} +`; + } + + result += ` +@Override +protected Map> getFieldChanges() { + return Map.ofEntries( + Map.entry(${oldName}, ${columnMapName}) + ); +} +`; + + return result.trimStart(); + }, [oldEntityType, newEntityType, columnMapping]); + + useEffect(() => { + const canvas = canvasRef.current; + const context = canvasRef.current?.getContext('2d'); + if (!canvas || !context) { + return; + } + + let oldColumns = oldEntityType?.columns || []; + let newColumns = newEntityType?.columns || []; + + if (!showMatched) { + oldColumns = oldColumns.filter((c) => !columnMapping[c.name]); + newColumns = newColumns.filter((c) => !Object.values(columnMapping).includes(c.name)); + } + + oldColumns = oldColumns.toSorted((a, b) => a.name.localeCompare(b.name)); + newColumns = newColumns.toSorted((a, b) => a.name.localeCompare(b.name)); + + const clickedColumn = oldColumns.find((c) => c.name === clicked); + if (showOnlyMatchingDataTypes && clickedColumn) { + newColumns = newColumns.filter((column) => column.dataType.dataType === clickedColumn?.dataType.dataType); + } + + const canvasWidth = canvas.getBoundingClientRect().width; + context.canvas.width = canvasWidth; + + const canvasHeight = Math.max(oldColumns.length, newColumns.length) * 24 + 24 * 2; + context.canvas.height = canvasHeight; + context.canvas.style.height = canvasHeight + 'px'; + + context.reset(); + context.font = '16px monospace'; + + const columnWidth = canvasWidth * 0.4; + + context.textAlign = 'right'; + context.font = 'bold 16px monospace'; + context.fillText(oldEntityType?.name ?? 'Old Entity Type', columnWidth, 16, columnWidth); + + context.font = '16px monospace'; + for (let i = 0; i < oldColumns.length; i++) { + const column = oldColumns[i]; + + if (columnMapping[column.name]) { + context.fillStyle = 'grey'; + } else { + context.fillStyle = 'black'; + } + + context.fillText(column.name, columnWidth, (i + 2) * 24, columnWidth); + context.strokeRect(1, (i + 2) * 24 - 16, columnWidth + 4, 24); + } + + context.textAlign = 'left'; + context.font = 'bold 16px monospace'; + context.fillText(newEntityType?.name ?? 'New Entity Type', canvasWidth - columnWidth, 16, columnWidth); + + for (let i = 0; i < newColumns.length; i++) { + const column = newColumns[i]; + + context.font = '16px monospace'; + if (Object.values(columnMapping).includes(column.name)) { + context.fillStyle = 'grey'; + } else if (column.dataType.dataType === clickedColumn?.dataType.dataType) { + context.font = 'bold 16px monospace'; + context.fillStyle = 'green'; + } else { + context.fillStyle = 'black'; + } + context.fillText(column.name, canvasWidth - columnWidth, (i + 2) * 24, columnWidth); + context.strokeRect(canvasWidth - columnWidth - 4, (i + 2) * 24 - 16, columnWidth + 4, 24); + } + + for (const [oldName, newName] of Object.entries(columnMapping)) { + const oldIndex = oldColumns.findIndex((c) => c.name === oldName); + const newIndex = newColumns.findIndex((c) => c.name === newName); + + if (oldIndex >= 0 && newIndex >= 0) { + context.beginPath(); + context.strokeStyle = 'black'; + context.lineWidth = 2; + context.moveTo(columnWidth + 4, 24 * oldIndex - 4 + 48); + context.lineTo(canvasWidth - columnWidth - 4, 24 * newIndex - 4 + 48); + context.stroke(); + } + } + + if (clicked) { + context.beginPath(); + context.strokeStyle = 'red'; + context.lineWidth = 4; + context.moveTo(columnWidth + 4, 24 * oldColumns.findIndex((c) => c.name === clicked) - 4 + 48); + context.lineTo(mousePosition.x, mousePosition.y); + context.stroke(); + } + + context.canvas.onmousedown = (e) => { + // if old column, set clicked = that + const x = e.clientX - canvas.getBoundingClientRect().left; + const y = e.clientY - canvas.getBoundingClientRect().top; + const index = Math.floor((y - 28) / 24); + + if (x < columnWidth && clicked !== null) { + delete columnMapping[clicked]; + setColumnMapping({ ...columnMapping }); + setClicked(null); + } else if (x < columnWidth) { + if (index >= 0 && index < oldColumns.length) { + setClicked(oldColumns[index].name); + } + } else if (clicked && x > canvasWidth - columnWidth) { + if (index >= 0 && index < newColumns.length) { + setColumnMapping((prev) => ({ ...prev, [clicked]: newColumns[index].name })); + setClicked(null); + } + } + }; + }, [oldEntityType, newEntityType, columnMapping, mousePosition, clicked, showMatched, showOnlyMatchingDataTypes]); + + return ( + + + + { + try { + setOldEntityType(json5.parse(s)); + } catch (e) { + setOldEntityType(null); + } + }} + extensions={[json(), EditorView.lineWrapping]} + /> + + + + { + try { + setNewEntityType(json5.parse(s)); + } catch (e) { + setNewEntityType(null); + } + }} + extensions={[json(), EditorView.lineWrapping]} + /> + + + + + + + setShowMatched(e.target.checked)} />} + label="Show matched columns" + /> + setShowOnlyMatchingDataTypes(e.target.checked)} + /> + } + label="Show only matching data types when matching" + /> + + + { + const rect = (e.target as HTMLCanvasElement).getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + setMousePosition({ x, y }); + }} + /> + + + + + + + + + ); +} diff --git a/entity-type-creator/package.json b/entity-type-creator/package.json index d29668b9..112fdd8a 100644 --- a/entity-type-creator/package.json +++ b/entity-type-creator/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@codemirror/lang-java": "^6.0.1", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.6.2", "@emotion/cache": "^11.11.0", diff --git a/entity-type-creator/pages/index.tsx b/entity-type-creator/pages/index.tsx index 6a025de8..c948896e 100644 --- a/entity-type-creator/pages/index.tsx +++ b/entity-type-creator/pages/index.tsx @@ -2,6 +2,7 @@ import CheckValidity from '@/components/CheckValidity'; import DBInspector from '@/components/DBInspector'; import EntityTypeManager from '@/components/EntityTypeManager'; import FqmConnector from '@/components/FqmConnector'; +import MigrationHelper from '@/components/MigrationHelper'; import ModuleInstaller from '@/components/ModuleInstaller'; import PostgresConnector from '@/components/PostgresConnector'; import QueryTool from '@/components/QueryTool'; @@ -75,7 +76,7 @@ export default function EntryPoint() { { - if (n === 5) { + if (n === 6) { setExpandedBottom((e) => !e); } else { setSelectedTab(n); @@ -88,6 +89,7 @@ export default function EntryPoint() { + @@ -104,6 +106,9 @@ export default function EntryPoint() { + + + diff --git a/pom.xml b/pom.xml index 1942e1e8..67b76d8d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ 8.1.0 2.1.0-SNAPSHOT 1.5.2.Final - 2.0.0 + 2.1.0-SNAPSHOT 4.0.0 2.0 42.5.4 diff --git a/src/main/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategy.java b/src/main/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategy.java new file mode 100644 index 00000000..b9a752d2 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategy.java @@ -0,0 +1,191 @@ +package org.folio.fqm.migration; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import lombok.extern.log4j.Log4j2; +import org.folio.fql.service.FqlService; +import org.folio.fqm.migration.warnings.EntityTypeWarning; +import org.folio.fqm.migration.warnings.FieldWarning; +import org.folio.fqm.migration.warnings.QueryBreakingWarning; +import org.folio.fqm.migration.warnings.RemovedEntityWarning; +import org.folio.fqm.migration.warnings.RemovedFieldWarning; +import org.folio.fqm.migration.warnings.Warning; +import org.folio.fqm.service.MigrationService; + +@Log4j2 +public abstract class AbstractSimpleMigrationStrategy implements MigrationStrategy { + + protected final ObjectMapper objectMapper = new ObjectMapper(); + + /** The version migrating FROM */ + public abstract String getSourceVersion(); + + /** The version migrating TO */ + public abstract String getTargetVersion(); + + /** The entity types that got a new UUID */ + public abstract Map getEntityTypeChanges(); + + /** + * The fields that were renamed. Keys should use their OLD entity type ID, if applicable. + * + * The special key "*" on the field map can be used to apply a template to all fields. This value + * should be a string with a %s placeholder, which will be filled in with the original field name. + */ + public abstract Map> getFieldChanges(); + + /** + * Entity types that were removed or deprecated. Removed ones will automatically be mapped to the `removed` entity type. + */ + public abstract Map> getEntityTypeWarnings(); + + /** + * The fields that were deprecated, removed, etc. + * + * ET keys should use their OLD entity type ID, if applicable. + * Fields should use their OLD names, if applicable. + * + * The function will be given the field's name and FQL (if applicable), as a string + */ + public abstract Map>> getFieldWarnings(); + + @Override + public boolean applies(String version) { + return this.getSourceVersion().equals(version); + } + + @Override + public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryInformation src) { + try { + MigratableQueryInformation result = src; + Set warnings = new HashSet<>(src.warnings()); + + Optional entityTypeWarning = Optional + .ofNullable(getEntityTypeWarnings().get(src.entityTypeId())) + .map(f -> f.apply(src.fqlQuery())); + if (entityTypeWarning.isPresent() && entityTypeWarning.get() instanceof RemovedEntityWarning) { + return MigratableQueryInformation + .builder() + .entityTypeId(MigrationService.REMOVED_ENTITY_TYPE_ID) + .fqlQuery(objectMapper.writeValueAsString(Map.of(MigrationService.VERSION_KEY, this.getTargetVersion()))) + .fields(List.of()) + .warning(getEntityTypeWarnings().get(src.entityTypeId()).apply(src.fqlQuery())) + .build(); + } else if (entityTypeWarning.isPresent()) { + warnings.add(entityTypeWarning.get()); + } + + if (src.fqlQuery() == null) { + result = + result.withFqlQuery( + objectMapper.writeValueAsString(Map.of(MigrationService.VERSION_KEY, this.getTargetVersion())) + ); + } + + result = + result.withEntityTypeId(this.getEntityTypeChanges().getOrDefault(src.entityTypeId(), src.entityTypeId())); + + ObjectNode fql = (ObjectNode) objectMapper.readTree(result.fqlQuery()); + fql.set(MigrationService.VERSION_KEY, objectMapper.valueToTree(this.getTargetVersion())); + + Map fieldChanges = this.getFieldChanges().getOrDefault(src.entityTypeId(), Map.of()); + Map> fieldWarnings = + this.getFieldWarnings().getOrDefault(src.entityTypeId(), Map.of()); + if (!fieldChanges.isEmpty() || !fieldWarnings.isEmpty()) { + // map query fields + fql = migrateFqlTree(fieldChanges, fieldWarnings, fql, warnings); + + // map fields list + result = + result.withFields( + src + .fields() + .stream() + .map(f -> { + if (fieldWarnings.containsKey(f)) { + Warning warning = fieldWarnings.get(f).apply(f, null); + if (!(warning instanceof QueryBreakingWarning)) { + warnings.add(warning); + } + if (warning instanceof RemovedFieldWarning) { + return null; + } + } + return getNewFieldName(fieldChanges, f); + }) + .filter(f -> f != null) + .distinct() + .toList() + ); + } + + result = result.withFqlQuery(objectMapper.writeValueAsString(fql)); + + return result.withWarnings(new ArrayList<>(warnings)); + } catch (JsonProcessingException e) { + log.error("Failed to serialize FQL query", e); + throw new UncheckedIOException(e); + } + } + + protected static String getNewFieldName(Map fieldChanges, String oldFieldName) { + if (MigrationService.VERSION_KEY.equals(oldFieldName)) { + return oldFieldName; + } else if (fieldChanges.containsKey("*")) { + return fieldChanges.get("*").formatted(oldFieldName); + } else { + return fieldChanges.getOrDefault(oldFieldName, oldFieldName); + } + } + + protected static ObjectNode migrateFqlTree( + Map fieldChanges, + Map> fieldWarnings, + ObjectNode fql, + Set warnings + ) { + ObjectNode result = new ObjectMapper().createObjectNode(); + // iterate through fields in source + fql + .fields() + .forEachRemaining(entry -> { + if ("$and".equals(entry.getKey())) { + ArrayNode resultContents = new ObjectMapper().createArrayNode(); + ((ArrayNode) entry.getValue()).elements() + .forEachRemaining(node -> { + ObjectNode innerResult = migrateFqlTree(fieldChanges, fieldWarnings, (ObjectNode) node, warnings); + // handle removed fields + if (!innerResult.isEmpty()) { + resultContents.add(innerResult); + } + }); + result.set("$and", resultContents); + } else { + if (fieldWarnings.containsKey(entry.getKey())) { + FieldWarning warning = fieldWarnings + .get(entry.getKey()) + .apply(entry.getKey(), entry.getValue().toPrettyString()); + warnings.add(warning); + if (warning instanceof RemovedFieldWarning || warning instanceof QueryBreakingWarning) { + return; + } + } + result.set(getNewFieldName(fieldChanges, entry.getKey()), entry.getValue()); + } + }); + + return result; + } +} diff --git a/src/main/java/org/folio/fqm/migration/MigratableQueryInformation.java b/src/main/java/org/folio/fqm/migration/MigratableQueryInformation.java new file mode 100644 index 00000000..18870b0c --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/MigratableQueryInformation.java @@ -0,0 +1,22 @@ +package org.folio.fqm.migration; + +import java.util.List; +import java.util.UUID; +import javax.annotation.CheckForNull; +import lombok.Builder; +import lombok.Singular; +import lombok.With; +import org.folio.fqm.migration.warnings.Warning; + +@With +@Builder +public record MigratableQueryInformation( + UUID entityTypeId, + @CheckForNull String fqlQuery, + List fields, + @Singular List warnings +) { + public MigratableQueryInformation(UUID entityTypeId, String fqlQuery, List fields) { + this(entityTypeId, fqlQuery, fields, List.of()); + } +} diff --git a/src/main/java/org/folio/fqm/migration/MigrationStrategy.java b/src/main/java/org/folio/fqm/migration/MigrationStrategy.java new file mode 100644 index 00000000..5f9e1b50 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/MigrationStrategy.java @@ -0,0 +1,25 @@ +package org.folio.fqm.migration; + +import jakarta.annotation.Nonnull; +import java.util.function.BiFunction; +import org.folio.fql.service.FqlService; + +public interface MigrationStrategy + extends BiFunction { + /** For logging purposes */ + String getLabel(); + + /** + * Determine if a query should be migrated by this strategy (if this strategy "applies" to a query) + */ + boolean applies(@Nonnull String version); + + /** + * Migrate the query. This method will be called iff {@link #applies(FqlService, MigratableQueryInformation)} returns true. + * + * After this method is called, {@link #applies(FqlService, MigratableQueryInformation)} MUST return false. Otherwise, an infinite + * loop will occur. + */ + @Override // respecified to add docblock + MigratableQueryInformation apply(FqlService fqlService, MigratableQueryInformation migratableQueryInformation); +} diff --git a/src/main/java/org/folio/fqm/migration/MigrationStrategyRepository.java b/src/main/java/org/folio/fqm/migration/MigrationStrategyRepository.java new file mode 100644 index 00000000..d36d0b19 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/MigrationStrategyRepository.java @@ -0,0 +1,19 @@ +package org.folio.fqm.migration; + +import java.util.Collections; +import java.util.List; +import org.folio.fqm.migration.strategies.V0POCMigration; +import org.springframework.stereotype.Component; + +@Component +public class MigrationStrategyRepository { + + // prevent re-initialization on each call + private static final List MIGRATION_STRATEGIES = Collections.unmodifiableList( + List.of(new V0POCMigration()) + ); + + public List getMigrationStrategies() { + return MIGRATION_STRATEGIES; + } +} diff --git a/src/main/java/org/folio/fqm/migration/strategies/V0POCMigration.java b/src/main/java/org/folio/fqm/migration/strategies/V0POCMigration.java new file mode 100644 index 00000000..9e576429 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/strategies/V0POCMigration.java @@ -0,0 +1,464 @@ +package org.folio.fqm.migration.strategies; + +import java.util.Map; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.folio.fqm.migration.AbstractSimpleMigrationStrategy; +import org.folio.fqm.migration.warnings.EntityTypeWarning; +import org.folio.fqm.migration.warnings.FieldWarning; +import org.folio.fqm.migration.warnings.QueryBreakingWarning; +import org.folio.fqm.migration.warnings.RemovedEntityWarning; + +/** + * Version 0 -> 1, original proof-of-concept changes (creation of complex entity types, etc). + * Implemented July 2024 based on changes in commit 9a1e180186dc7178b0c3416f1aec088c9ce39a54. + */ +@SuppressWarnings("java:S1192") // allow constant string duplication +public class V0POCMigration extends AbstractSimpleMigrationStrategy { + + public static final UUID OLD_DRV_LOAN_DETAILS = UUID.fromString("4e09d89a-44ed-418e-a9cc-820dfb27bf3a"); + public static final UUID NEW_COMPOSITE_LOAN_DETAILS = UUID.fromString("d6729885-f2fb-4dc7-b7d0-a865a7f461e4"); + public static final Map DRV_LOAN_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("holdings_id", "holdings.id"), + Map.entry("instance_id", "instance.id"), + Map.entry("instance_primary_contributor", "instance.contributors"), + Map.entry("instance_title", "instance.title"), + Map.entry("item_barcode", "items.barcode"), + Map.entry("item_call_number", "items.effective_call_number"), + Map.entry("item_id", "items.id"), + Map.entry("item_material_type", "mtypes.name"), + Map.entry("item_material_type_id", "mtypes.id"), + Map.entry("item_status", "items.status_name"), + Map.entry("loan_checkin_servicepoint_id", "cispi.id"), + Map.entry("loan_checkin_servicepoint_name", "cispi.name"), + Map.entry("loan_checkout_servicepoint_id", "cospi.id"), + Map.entry("loan_checkout_servicepoint_name", "cospi.name"), + Map.entry("loan_checkout_date", "loans.loan_date"), + Map.entry("id", "loans.id"), + Map.entry("loan_policy_id", "lpolicy.id"), + Map.entry("loan_policy_name", "lpolicy.name"), + Map.entry("loan_return_date", "loans.return_date"), + Map.entry("loan_status", "loans.status_name"), + Map.entry("user_active", "users.active"), + Map.entry("user_barcode", "users.barcode"), + Map.entry("user_expiration_date", "users.expiration_date"), + Map.entry("user_first_name", "users.first_name"), + Map.entry("user_full_name", "users.last_name_first_name"), + Map.entry("user_id", "users.id"), + Map.entry("loan_due_date", "loans.due_date"), + Map.entry("user_last_name", "users.last_name"), + Map.entry("user_patron_group", "groups.group"), + Map.entry("user_patron_group_id", "groups.id"), + // this column did not exist in the actual entity type, but it is (incorrectly) referenced by a canned list; we should fix that: + Map.entry("item_holdingsrecord_id", "items.hrid") + ); + + public static final UUID OLD_SRC_CIRCULATION_LOAN_POLICY = UUID.fromString("5e7de445-bcc6-4008-8032-8d9602b854d7"); + public static final UUID NEW_SIMPLE_LOAN_POLICY = UUID.fromString("64d7b5fb-2ead-444c-a9bd-b9db831f4132"); + public static final Map SRC_CIRCULATION_LOAN_POLICY_COLUMN_MAPPING = Map.ofEntries( + Map.entry("policy_name", "name") + ); + + // drv_holdings_record_details was renamed to composite_holdings_record; same ID + public static final UUID DRV_HOLDINGS_RECORD_DETAILS = UUID.fromString("8418e512-feac-4a6a-a56d-9006aab31e33"); + public static final Map DRV_HOLDINGS_RECORD_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("holdings_effective_location", "effective_location.name"), + Map.entry("holdings_effective_location_id", "effective_location.id"), + Map.entry("holdings_effective_library_code", "effective_library.code"), + Map.entry("holdings_effective_library_name", "effective_library.name"), + Map.entry("holdings_effective_library_id", "effective_library.id"), + Map.entry("holdings_hrid", "holdings.hrid"), + Map.entry("id", "holdings.id"), + Map.entry("holdings_permanent_location", "permanent_location.name"), + Map.entry("holdings_permanent_location_id", "permanent_location.id"), + Map.entry("holdings_statistical_code_ids", "holdings.statistical_code_ids"), + Map.entry("holdings_statistical_codes", "holdings.statistical_code_names"), + Map.entry("holdings_suppress_from_discovery", "holdings.discovery_suppress"), + Map.entry("holdings_temporary_location", "temporary_location.name"), + Map.entry("holdings_temporary_location_id", "temporary_location.id"), + Map.entry("instance_id", "holdings.instance_id") + ); + + // drv_instances was renamed to composite_instances; same ID + public static final UUID DRV_INSTANCES = UUID.fromString("6b08439b-4f8e-4468-8046-ea620f5cfb74"); + public static final Map DRV_INSTANCES_COLUMN_MAPPING = Map.ofEntries( + Map.entry("instance_cataloged_date", "instance.cataloged_date"), + Map.entry("instance_metadata_created_date", "instance.created_at"), + Map.entry("instance_hrid", "instance.hrid"), + Map.entry("id", "instance.id"), + Map.entry("instance_title", "instance.title"), + Map.entry("instance_discovery_suppress", "instance.discovery_suppress"), + Map.entry("instance_metadata_updated_date", "instance.updated_at"), + Map.entry("instance_statistical_code_ids", "instance.statistical_code_ids"), + Map.entry("instance_statistical_codes", "instance.statistical_code_names"), + Map.entry("instance_status_id", "instance.status_id"), + Map.entry("instance_status", "instance.status"), + Map.entry("mode_of_issuance_id", "mode_of_issuance.id"), + Map.entry("mode_of_issuance", "mode_of_issuance.name"), + Map.entry("instance_source", "instance.source"), + Map.entry("instance_contributor_type_ids", "instance.contributors[*]->contributor_type_id"), + Map.entry("instance_contributor_type", "instance.contributors[*]->contributor_type_text"), + Map.entry("instance_contributor_type_name_ids", "instance.contributors[*]->contributor_name_type_id"), + Map.entry("instance_contributor_name_type", "instance.contributors[*]->name"), + Map.entry("instance_language", "instance.languages") + ); + + public static final UUID OLD_DRV_ITEM_DETAILS = UUID.fromString("0cb79a4c-f7eb-4941-a104-745224ae0292"); + public static final UUID NEW_COMPOSITE_ITEM_DETAILS = UUID.fromString("d0213d22-32cf-490f-9196-d81c3c66e53f"); + public static final Map DRV_ITEM_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("holdings_id", "holdings.id"), + Map.entry("id", "items.id"), + Map.entry("instance_created_date", "instances.created_at"), + Map.entry("instance_id", "instances.id"), + Map.entry("instance_primary_contributor", "instances.contributors"), + Map.entry("instance_title", "instances.title"), + Map.entry("instance_updated_date", "instances.updated_at"), + Map.entry("item_barcode", "items.barcode"), + Map.entry("item_copy_number", "items.copy_number"), + Map.entry("item_created_date", "items.created_date"), + Map.entry("item_effective_call_number", "items.effective_call_number"), + Map.entry("item_effective_call_number_type_name", "effective_call_number.name"), + Map.entry("item_effective_call_number_typeid", "effective_call_number.id"), + Map.entry("item_effective_library_code", "loclibrary.code"), + Map.entry("item_effective_library_id", "loclibrary.id"), + Map.entry("item_effective_library_name", "loclibrary.name"), + Map.entry("item_effective_location_id", "effective_location.id"), + Map.entry("item_effective_location_name", "effective_location.name"), + Map.entry("item_hrid", "items.hrid"), + Map.entry("item_level_call_number", "items.item_level_call_number"), + Map.entry("item_level_call_number_type_name", "item_level_call_number.name"), + Map.entry("item_level_call_number_typeid", "item_level_call_number.id"), + Map.entry("item_material_type", "mtypes.name"), + Map.entry("item_material_type_id", "mtypes.id"), + Map.entry("item_permanent_location_id", "permanent_location.id"), + Map.entry("item_permanent_location_name", "permanent_location.name"), + Map.entry("item_statistical_code_ids", "items.statistical_code_ids"), + Map.entry("item_statistical_codes", "items.statistical_code_names"), + Map.entry("item_status", "items.status_name"), + Map.entry("item_temporary_location_id", "temporary_location.id"), + Map.entry("item_temporary_location_name", "temporary_location.name"), + Map.entry("item_updated_date", "items.updated_date"), + // this column did not exist in the actual entity type, but it is (incorrectly) referenced by a canned list; we should fix that: + Map.entry("item_holdings_record_id", "items.hrid") + ); + + public static final UUID OLD_SRC_INVENTORY_CALL_NUMBER_TYPE = UUID.fromString("5c8315be-13f5-4df5-ae8b-086bae83484d"); + public static final UUID NEW_SIMPLE_CALL_NUMBER_TYPE = UUID.fromString("d9338ced-3e71-4f24-b605-7912d590f005"); + public static final Map SRC_INVENTORY_CALL_NUMBER_TYPE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("call_number_type_name", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_CONTRIBUTOR_NAME_TYPE = UUID.fromString( + "9c24a719-679b-4cca-9146-42a46d721df5" + ); + public static final UUID NEW_DRV_CONTRIBUTOR_NAME_TYPE_DETAILS = UUID.fromString( + "6cd60e0e-f862-413a-9857-1d1ef1ca34ea" + ); + public static final Map SRC_INVENTORY_CONTRIBUTOR_NAME_TYPE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("contributor_name_type", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_CONTRIBUTOR_TYPE = UUID.fromString("3553ca38-d522-439b-9f91-1512275a43b9"); + public static final UUID NEW_DRV_CONTRIBUTOR_TYPE_DETAILS = UUID.fromString("a09e4959-3a6f-4fc6-a8a8-16bb9d30dba2"); + public static final Map SRC_INVENTORY_CONTRIBUTOR_TYPE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("contributor_type", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_INSTANCE_STATUS = UUID.fromString("bc03686c-657e-4f74-9d89-91eac5ea86a4"); + public static final UUID NEW_SIMPLE_INSTANCE_STATUS = UUID.fromString("9c239bfd-198f-4013-bbc4-4551c0cbdeaa"); + public static final Map SRC_INVENTORY_INSTANCE_STATUS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("status", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_LOCATION = UUID.fromString("a9d6305e-fdb4-4fc4-8a73-4a5f76d8410b"); + public static final UUID NEW_SIMPLE_LOCATIONS = UUID.fromString("74ddf1a6-19e0-4d63-baf0-cd2da9a46ca4"); + public static final Map SRC_INVENTORY_LOCATION_COLUMN_MAPPING = Map.ofEntries( + Map.entry("location_code", "code"), + Map.entry("location_name", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_LOCLIBRARY = UUID.fromString("cf9f5c11-e943-483c-913b-81d1e338accc"); + public static final UUID NEW_SIMPLE_LOCLIBRARY = UUID.fromString("32f58888-1a7b-4840-98f8-cc69ca93fc67"); + public static final Map SRC_INVENTORY_LOCLIBRARY_COLUMN_MAPPING = Map.ofEntries( + Map.entry("loclibrary_code", "code"), + Map.entry("loclibrary_name", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_MATERIAL_TYPE = UUID.fromString("917ea5c8-cafe-4fa6-a942-e2388a88c6f6"); + public static final UUID NEW_SIMPLE_MATERIAL_TYPE_DETAILS = UUID.fromString("8b1f51d6-8795-4113-a72e-3b7dc6cc6dfe"); + public static final Map SRC_INVENTORY_MATERIAL_TYPE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("material_type_name", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_MODE_OF_ISSUANCE = UUID.fromString("60e315d6-db28-4077-9277-b946411fe7d9"); + public static final UUID NEW_SIMPLE_MODE_OF_ISSUANCE = UUID.fromString("073b554a-5b5c-4552-a51c-01448a1643b0"); + public static final Map SRC_INVENTORY_MODE_OF_ISSUANCE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("mode_of_issuance", "name") + ); + + public static final UUID OLD_SRC_INVENTORY_SERVICE_POINT = UUID.fromString("89cdeac4-9582-4388-800b-9ccffd8d7691"); + public static final UUID NEW_SIMPLE_SERVICE_POINT_DETAIL = UUID.fromString("1fdcc2e8-1ff8-4a99-b4ad-7d6bf564aec5"); + public static final Map SRC_INVENTORY_SERVICE_POINT_COLUMN_MAPPING = Map.ofEntries( + Map.entry("service_point_code", "code"), + Map.entry("service_point_name", "name") + ); + + public static final UUID OLD_DRV_ORGANIZATION_CONTACTS = UUID.fromString("7a7860cd-e939-504f-b51f-ed3e1e6b12b9"); + public static final UUID NEW_SIMPLE_ORGANIZATION = UUID.fromString("b5ffa2e9-8080-471a-8003-a8c5a1274503"); + public static final Map DRV_ORGANIZATION_CONTACTS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("acquisition_unit_id", "acq_unit_ids"), + Map.entry("acquisition_unit_name", "acq_unit_names"), + Map.entry("address", "addresses"), + Map.entry("alias", "aliases"), + Map.entry("email", "emails"), + Map.entry("last_updated", "updated_at"), + Map.entry("organization_status", "status"), + Map.entry("organization_type_ids", "type_ids"), + Map.entry("organization_type_name", "type_names"), + Map.entry("phone_number", "phone_numbers"), + Map.entry("url", "urls") + ); + + public static final UUID OLD_DRV_ORGANIZATION_DETAILS = UUID.fromString("837f262e-2073-4a00-8bcc-4e4ce6e669b3"); + public static final Map DRV_ORGANIZATION_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("acquisition_unit", "acq_unit_names"), + Map.entry("acqunit_ids", "acq_unit_ids"), + Map.entry("alias", "aliases"), + Map.entry("last_updated", "updated_at"), + Map.entry("organization_status", "status"), + Map.entry("organization_type_ids", "type_ids"), + Map.entry("organization_type_name", "type_names") + ); + + public static final UUID OLD_SRC_ORGANIZATION_TYPES = UUID.fromString("6b335e41-2654-4e2a-9b4e-c6930b330ccc"); + public static final UUID NEW_SIMPLE_ORGANIZATION_TYPES = UUID.fromString("85a2b008-af8d-4890-9490-421cabcb7bad"); + public static final Map SRC_ORGANIZATION_TYPES_COLUMN_MAPPING = Map.ofEntries( + Map.entry("organization_types_name", "name") + ); + + public static final UUID OLD_SRC_ORGANIZATIONS = UUID.fromString("489234a9-8703-48cd-85e3-7f84011bafa3"); + public static final Map SRC_ORGANIZATIONS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("vendor_code", "code"), + Map.entry("vendor_name", "name") + ); + + public static final UUID OLD_DRV_PURCHASE_ORDER_LINE_DETAILS = UUID.fromString( + "90403847-8c47-4f58-b117-9a807b052808" + ); + public static final UUID NEW_COMPOSITE_PURCHASE_ORDER_LINES = UUID.fromString("abc777d3-2a45-43e6-82cb-71e8c96d13d2"); + public static final Map DRV_PURCHASE_ORDER_LINE_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("acquisition_unit", "po.acquisition_unit"), + Map.entry("acqunit_ids", "po.acq_unit_ids"), + Map.entry("fund_distribution", "pol.fund_distribution"), + Map.entry("id", "pol.id"), + Map.entry("po_approved", "po.approved"), + Map.entry("po_assigned_to", "assigned_to_user.last_name_first_name"), + Map.entry("po_assigned_to_id", "assigned_to_user.id"), + Map.entry("po_created_by", "po_created_by_user.last_name_first_name"), + Map.entry("po_created_by_id", "po_created_by_user.id"), + Map.entry("po_created_date", "po.created_at"), + Map.entry("po_id", "po.id"), + Map.entry("po_notes", "po.notes"), + Map.entry("po_number", "po.po_number"), + Map.entry("po_type", "po.order_type"), + Map.entry("po_updated_by", "po_updated_by_user.last_name_first_name"), + Map.entry("po_updated_by_id", "po_updated_by_user.id"), + Map.entry("po_updated_date", "po.updated_at"), + Map.entry("po_workflow_status", "po.workflow_status"), + Map.entry("pol_created_by", "pol_created_by_user.last_name_first_name"), + Map.entry("pol_created_by_id", "pol_created_by_user.id"), + Map.entry("pol_created_date", "pol.created_at"), + Map.entry("pol_currency", "pol.cost_currency"), + Map.entry("pol_description", "pol.description"), + Map.entry("pol_estimated_price", "pol.cost_po_line_estimated_price"), + Map.entry("pol_exchange_rate", "pol.cost_exchange_rate"), + Map.entry("pol_number", "pol.po_line_number"), + Map.entry("pol_payment_status", "pol.payment_status"), + Map.entry("pol_receipt_status", "pol.receipt_status"), + Map.entry("pol_updated_by", "pol_updated_by_user.last_name_first_name"), + Map.entry("pol_updated_by_id", "pol_updated_by_user.id"), + Map.entry("pol_updated_date", "pol.updated_at"), + Map.entry("vendor_code", "vendor_organization.code"), + Map.entry("vendor_id", "vendor_organization.id"), + Map.entry("vendor_name", "vendor_organization.name") + ); + + public static final UUID OLD_DRV_USER_DETAILS = UUID.fromString("0069cf6f-2833-46db-8a51-8934769b8289"); + public static final UUID NEW_COMPOSITE_USER_DETAILS = UUID.fromString("ddc93926-d15a-4a45-9d9c-93eadc3d9bbf"); + public static final Map DRV_USER_DETAILS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("id", "users.id"), + Map.entry("user_active", "users.active"), + Map.entry("user_address_ids", "users.addresses[*]->address_id"), + Map.entry("user_address_line1", "users.addresses[*]->address_line1"), + Map.entry("user_address_line2", "users.addresses[*]->address_line2"), + Map.entry("user_barcode", "users.barcode"), + Map.entry("user_cities", "users.addresses[*]->city"), + Map.entry("user_country_ids", "users.addresses[*]->country_id"), + Map.entry("user_created_date", "users.created_date"), + Map.entry("user_date_of_birth", "users.date_of_birth"), + Map.entry("user_department_ids", "users.department_ids"), + Map.entry("user_department_names", "users.departments"), + Map.entry("user_email", "users.email"), + Map.entry("user_enrollment_date", "users.enrollment_date"), + Map.entry("user_expiration_date", "users.expiration_date"), + Map.entry("user_external_system_id", "users.external_system_id"), + Map.entry("user_first_name", "users.first_name"), + Map.entry("user_last_name", "users.last_name"), + Map.entry("user_middle_name", "users.middle_name"), + Map.entry("user_mobile_phone", "users.mobile_phone"), + Map.entry("user_patron_group", "groups.group"), + Map.entry("user_patron_group_id", "groups.id"), + Map.entry("user_phone", "users.phone"), + Map.entry("user_postal_codes", "users.addresses[*]->postal_code"), + Map.entry("user_preferred_contact_type", "users.preferred_contact_type"), + Map.entry("user_preferred_first_name", "users.preferred_first_name"), + Map.entry("user_primary_address", "users.addresses"), + Map.entry("user_regions", "users.addresses[*]->region"), + Map.entry("user_updated_date", "users.updated_date"), + Map.entry("username", "users.username") + ); + + public static final UUID OLD_SRC_USERS_ADDRESSTYPE = UUID.fromString("e627a89b-682b-41fe-b532-f4262020a451"); + public static final UUID NEW_SIMPLE_ADDRESS_TYPES = UUID.fromString("9176c676-0485-4f6c-b1fc-585355bac679"); + public static final Map SRC_USERS_ADDRESSTYPE_COLUMN_MAPPING = Map.ofEntries( + Map.entry("addressType", "type") + ); + + public static final UUID OLD_SRC_USERS_DEPARTMENTS = UUID.fromString("c8364551-7e51-475d-8473-88951181452d"); + public static final UUID NEW_SIMPLE_DEPARTMENT_DETAILS = UUID.fromString("f067beda-cbeb-4423-9a0d-3b59fb329ce2"); + public static final Map SRC_USERS_DEPARTMENTS_COLUMN_MAPPING = Map.ofEntries( + Map.entry("department", "name") + ); + + // no column changes + public static final UUID OLD_SRC_USERS_GROUPS = UUID.fromString("e611264d-377e-4d87-a93f-f1ca327d3db0"); + public static final UUID NEW_SIMPLE_GROUP_DETAILS = UUID.fromString("e7717b38-4ff3-4fb9-ae09-b3d0c8400710"); + + @Override + public String getLabel() { + return "V0 -> V1 POC Migration"; + } + + @Override + public String getSourceVersion() { + return "0"; + } + + @Override + public String getTargetVersion() { + return "1"; + } + + @Override + public Map getEntityTypeChanges() { + return Map.ofEntries( + Map.entry(OLD_DRV_LOAN_DETAILS, NEW_COMPOSITE_LOAN_DETAILS), + Map.entry(OLD_SRC_CIRCULATION_LOAN_POLICY, NEW_SIMPLE_LOAN_POLICY), + Map.entry(OLD_DRV_ITEM_DETAILS, NEW_COMPOSITE_ITEM_DETAILS), + Map.entry(OLD_SRC_INVENTORY_CALL_NUMBER_TYPE, NEW_SIMPLE_CALL_NUMBER_TYPE), + Map.entry(OLD_SRC_INVENTORY_CONTRIBUTOR_NAME_TYPE, NEW_DRV_CONTRIBUTOR_NAME_TYPE_DETAILS), + Map.entry(OLD_SRC_INVENTORY_CONTRIBUTOR_TYPE, NEW_DRV_CONTRIBUTOR_TYPE_DETAILS), + Map.entry(OLD_SRC_INVENTORY_INSTANCE_STATUS, NEW_SIMPLE_INSTANCE_STATUS), + Map.entry(OLD_SRC_INVENTORY_LOCATION, NEW_SIMPLE_LOCATIONS), + Map.entry(OLD_SRC_INVENTORY_LOCLIBRARY, NEW_SIMPLE_LOCLIBRARY), + Map.entry(OLD_SRC_INVENTORY_MATERIAL_TYPE, NEW_SIMPLE_MATERIAL_TYPE_DETAILS), + Map.entry(OLD_SRC_INVENTORY_MODE_OF_ISSUANCE, NEW_SIMPLE_MODE_OF_ISSUANCE), + Map.entry(OLD_SRC_INVENTORY_SERVICE_POINT, NEW_SIMPLE_SERVICE_POINT_DETAIL), + Map.entry(OLD_DRV_ORGANIZATION_CONTACTS, NEW_SIMPLE_ORGANIZATION), + Map.entry(OLD_DRV_ORGANIZATION_DETAILS, NEW_SIMPLE_ORGANIZATION), + Map.entry(OLD_SRC_ORGANIZATION_TYPES, NEW_SIMPLE_ORGANIZATION_TYPES), + Map.entry(OLD_SRC_ORGANIZATIONS, NEW_SIMPLE_ORGANIZATION), + Map.entry(OLD_DRV_PURCHASE_ORDER_LINE_DETAILS, NEW_COMPOSITE_PURCHASE_ORDER_LINES), + Map.entry(OLD_DRV_USER_DETAILS, NEW_COMPOSITE_USER_DETAILS), + Map.entry(OLD_SRC_USERS_ADDRESSTYPE, NEW_SIMPLE_ADDRESS_TYPES), + Map.entry(OLD_SRC_USERS_DEPARTMENTS, NEW_SIMPLE_DEPARTMENT_DETAILS), + Map.entry(OLD_SRC_USERS_GROUPS, NEW_SIMPLE_GROUP_DETAILS) + ); + } + + @Override + public Map> getFieldChanges() { + return Map.ofEntries( + Map.entry(OLD_DRV_LOAN_DETAILS, DRV_LOAN_DETAILS_COLUMN_MAPPING), + Map.entry(OLD_SRC_CIRCULATION_LOAN_POLICY, SRC_CIRCULATION_LOAN_POLICY_COLUMN_MAPPING), + Map.entry(DRV_HOLDINGS_RECORD_DETAILS, DRV_HOLDINGS_RECORD_DETAILS_COLUMN_MAPPING), + Map.entry(DRV_INSTANCES, DRV_INSTANCES_COLUMN_MAPPING), + Map.entry(OLD_DRV_ITEM_DETAILS, DRV_ITEM_DETAILS_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_CALL_NUMBER_TYPE, SRC_INVENTORY_CALL_NUMBER_TYPE_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_CONTRIBUTOR_NAME_TYPE, SRC_INVENTORY_CONTRIBUTOR_NAME_TYPE_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_CONTRIBUTOR_TYPE, SRC_INVENTORY_CONTRIBUTOR_TYPE_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_INSTANCE_STATUS, SRC_INVENTORY_INSTANCE_STATUS_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_LOCATION, SRC_INVENTORY_LOCATION_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_LOCLIBRARY, SRC_INVENTORY_LOCLIBRARY_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_MATERIAL_TYPE, SRC_INVENTORY_MATERIAL_TYPE_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_MODE_OF_ISSUANCE, SRC_INVENTORY_MODE_OF_ISSUANCE_COLUMN_MAPPING), + Map.entry(OLD_SRC_INVENTORY_SERVICE_POINT, SRC_INVENTORY_SERVICE_POINT_COLUMN_MAPPING), + Map.entry(OLD_DRV_ORGANIZATION_CONTACTS, DRV_ORGANIZATION_CONTACTS_COLUMN_MAPPING), + Map.entry(OLD_DRV_ORGANIZATION_DETAILS, DRV_ORGANIZATION_DETAILS_COLUMN_MAPPING), + Map.entry(OLD_SRC_ORGANIZATION_TYPES, SRC_ORGANIZATION_TYPES_COLUMN_MAPPING), + Map.entry(OLD_SRC_ORGANIZATIONS, SRC_ORGANIZATIONS_COLUMN_MAPPING), + Map.entry(OLD_DRV_PURCHASE_ORDER_LINE_DETAILS, DRV_PURCHASE_ORDER_LINE_DETAILS_COLUMN_MAPPING), + Map.entry(OLD_DRV_USER_DETAILS, DRV_USER_DETAILS_COLUMN_MAPPING), + Map.entry(OLD_SRC_USERS_ADDRESSTYPE, SRC_USERS_ADDRESSTYPE_COLUMN_MAPPING), + Map.entry(OLD_SRC_USERS_DEPARTMENTS, SRC_USERS_DEPARTMENTS_COLUMN_MAPPING) + ); + } + + @Override + public Map> getEntityTypeWarnings() { + return Map.ofEntries( + Map.entry( + UUID.fromString("146dfba5-cdc9-45f5-a8a1-3fdc454c9ae2"), + fql -> new RemovedEntityWarning("drv_loan_status", "simple_loans", fql) + ), + Map.entry( + UUID.fromString("097a6f96-edd0-11ed-a05b-0242ac120003"), + fql -> new RemovedEntityWarning("drv_item_callnumber_location", null, fql) + ), + Map.entry( + UUID.fromString("0cb79a4c-f7eb-4941-a104-745224ae0293"), + fql -> new RemovedEntityWarning("drv_item_holdingsrecord_instance", "composite_item_details", fql) + ), + Map.entry( + UUID.fromString("a1a37288-1afe-4fa5-ab59-a5bcf5d8ca2d"), + fql -> new RemovedEntityWarning("drv_item_status", null, fql) + ), + Map.entry( + UUID.fromString("5fefec2a-9d6c-474c-8698-b0ea77186c12"), + fql -> new RemovedEntityWarning("drv_pol_receipt_status", null, fql) + ), + Map.entry( + UUID.fromString("2168014f-9316-4760-9d82-d0306d5f59e4"), + fql -> new RemovedEntityWarning("drv_pol_payment_status", null, fql) + ) + ); + } + + @Override + public Map>> getFieldWarnings() { + return Map.ofEntries( + Map.entry( + OLD_DRV_LOAN_DETAILS, + Map.of( + "instance_primary_contributor", + (String field, String fql) -> new QueryBreakingWarning(field, "instance.contributors", fql) + ) + ), + Map.entry( + OLD_DRV_ITEM_DETAILS, + Map.of( + "instance_primary_contributor", + (String field, String fql) -> new QueryBreakingWarning(field, "instance.contributors", fql) + ) + ), + Map.entry( + OLD_DRV_USER_DETAILS, + Map.of( + "user_primary_address", + (String field, String fql) -> new QueryBreakingWarning(field, "users.addresses", fql) + ) + ) + ); + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/DeprecatedEntityWarning.java b/src/main/java/org/folio/fqm/migration/warnings/DeprecatedEntityWarning.java new file mode 100644 index 00000000..d63cea55 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/DeprecatedEntityWarning.java @@ -0,0 +1,47 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.folio.fqm.service.LocalizationService; +import org.folio.spring.i18n.service.TranslationService; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class DeprecatedEntityWarning implements EntityTypeWarning { + + public static final WarningType TYPE = WarningType.DEPRECATED_ENTITY; + + private final String entityType; + + @CheckForNull + private final String alternative; + + @Override + public WarningType getType() { + return TYPE; + } + + @Override + public String getDescription(TranslationService translationService) { + if (alternative != null) { + return translationService.format( + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(this.getType().toString()) + + ".withAlternative", + "name", + entityType, + "alternative", + alternative + ); + } else { + return translationService.format( + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(this.getType().toString()) + + ".withoutAlternative", + "name", + entityType + ); + } + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/DeprecatedFieldWarning.java b/src/main/java/org/folio/fqm/migration/warnings/DeprecatedFieldWarning.java new file mode 100644 index 00000000..42520fab --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/DeprecatedFieldWarning.java @@ -0,0 +1,45 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.folio.fqm.service.LocalizationService; +import org.folio.spring.i18n.service.TranslationService; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class DeprecatedFieldWarning implements FieldWarning { + + public static final WarningType TYPE = WarningType.DEPRECATED_FIELD; + + private final String field; + + @CheckForNull + private final String fql; + + @Override + public WarningType getType() { + return TYPE; + } + + @Override + public String getDescription(TranslationService translationService) { + if (fql != null) { + return translationService.format( + // we do not share the query itself here since the field is not removed from the query. + // the use of the `fql` parameter is just for a more informative warning, e.g. "in your query" vs "in your field list" + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(this.getType().toString()) + ".query", + "name", + field + ); + } else { + return translationService.format( + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(this.getType().toString()) + ".field", + "name", + field + ); + } + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/EntityTypeWarning.java b/src/main/java/org/folio/fqm/migration/warnings/EntityTypeWarning.java new file mode 100644 index 00000000..78766ae1 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/EntityTypeWarning.java @@ -0,0 +1,4 @@ +package org.folio.fqm.migration.warnings; + +// applicable for entity types only +public interface EntityTypeWarning extends Warning {} diff --git a/src/main/java/org/folio/fqm/migration/warnings/FieldWarning.java b/src/main/java/org/folio/fqm/migration/warnings/FieldWarning.java new file mode 100644 index 00000000..5bec8f03 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/FieldWarning.java @@ -0,0 +1,4 @@ +package org.folio.fqm.migration.warnings; + +// applicable for fields only +public interface FieldWarning extends Warning {} diff --git a/src/main/java/org/folio/fqm/migration/warnings/QueryBreakingWarning.java b/src/main/java/org/folio/fqm/migration/warnings/QueryBreakingWarning.java new file mode 100644 index 00000000..61892282 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/QueryBreakingWarning.java @@ -0,0 +1,33 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.folio.spring.i18n.service.TranslationService; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class QueryBreakingWarning implements FieldWarning { + + public static final WarningType TYPE = WarningType.QUERY_BREAKING; + + private final String field; + + @CheckForNull + private final String alternative; + + @CheckForNull + private final String fql; + + @Override + public WarningType getType() { + return TYPE; + } + + @Override + public String getDescription(TranslationService translationService) { + return Warning.getDescriptionByAlternativeAndFql(translationService, this.getType(), field, fql, alternative); + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/RemovedEntityWarning.java b/src/main/java/org/folio/fqm/migration/warnings/RemovedEntityWarning.java new file mode 100644 index 00000000..cadf1d72 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/RemovedEntityWarning.java @@ -0,0 +1,33 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.folio.spring.i18n.service.TranslationService; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class RemovedEntityWarning implements EntityTypeWarning { + + public static final WarningType TYPE = WarningType.REMOVED_ENTITY; + + private final String entityType; + + @CheckForNull + private final String alternative; + + @CheckForNull + private final String fql; + + @Override + public WarningType getType() { + return TYPE; + } + + @Override + public String getDescription(TranslationService translationService) { + return Warning.getDescriptionByAlternativeAndFql(translationService, TYPE, entityType, fql, alternative); + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/RemovedFieldWarning.java b/src/main/java/org/folio/fqm/migration/warnings/RemovedFieldWarning.java new file mode 100644 index 00000000..ebd32725 --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/RemovedFieldWarning.java @@ -0,0 +1,33 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.folio.spring.i18n.service.TranslationService; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class RemovedFieldWarning implements FieldWarning { + + public static final WarningType TYPE = WarningType.REMOVED_FIELD; + + private final String field; + + @CheckForNull + private final String alternative; + + @CheckForNull + private final String fql; + + @Override + public WarningType getType() { + return TYPE; + } + + @Override + public String getDescription(TranslationService translationService) { + return Warning.getDescriptionByAlternativeAndFql(translationService, this.getType(), field, fql, alternative); + } +} diff --git a/src/main/java/org/folio/fqm/migration/warnings/Warning.java b/src/main/java/org/folio/fqm/migration/warnings/Warning.java new file mode 100644 index 00000000..7a0d988d --- /dev/null +++ b/src/main/java/org/folio/fqm/migration/warnings/Warning.java @@ -0,0 +1,60 @@ +package org.folio.fqm.migration.warnings; + +import javax.annotation.CheckForNull; +import lombok.RequiredArgsConstructor; +import org.folio.fqm.service.LocalizationService; +import org.folio.spring.i18n.service.TranslationService; + +public interface Warning { + WarningType getType(); + String getDescription(TranslationService translationService); + + public static String getDescriptionByAlternativeAndFql( + TranslationService translationService, + WarningType type, + String name, + String fql, + @CheckForNull String alternative + ) { + if (alternative != null) { + return translationService.format( + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(type.toString()) + ".withAlternative", + "name", + name, + "alternative", + alternative, + "fql", + fql + ); + } else { + return translationService.format( + LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(type.toString()) + ".withoutAlternative", + "name", + name, + "fql", + fql + ); + } + } + + @RequiredArgsConstructor + public enum WarningType { + /** Only warns the user. Query and fields are unaffected */ + DEPRECATED_FIELD("DEPRECATED_FIELD"), + /** Only warns the user. Query and fields are unaffected */ + DEPRECATED_ENTITY("DEPRECATED_ENTITY"), + /** Query is broken and will not work with this field; we will remove the field from the query */ + QUERY_BREAKING("QUERY_BREAKING"), + /** This field is completely gone from both fields and queries */ + REMOVED_FIELD("REMOVED_FIELD"), + /** This entity type is completely gone */ + REMOVED_ENTITY("REMOVED_ENTITY"); + + private final String type; + + @Override + public String toString() { + return type; + } + } +} diff --git a/src/main/java/org/folio/fqm/service/LocalizationService.java b/src/main/java/org/folio/fqm/service/LocalizationService.java index 5d5caff2..2fd80e95 100644 --- a/src/main/java/org/folio/fqm/service/LocalizationService.java +++ b/src/main/java/org/folio/fqm/service/LocalizationService.java @@ -33,6 +33,9 @@ public class LocalizationService { // the translation parameter for custom fields private static final String CUSTOM_FIELD_PARAMETER = "customField"; + // translation logic happens in Warning classes + public static final String MIGRATION_WARNING_TRANSLATION_TEMPLATE = "mod-fqm-manager.migration.warning.%s"; + private TranslationService translationService; public EntityType localizeEntityType(EntityType entityType) { @@ -69,7 +72,7 @@ private String getSourceTranslationPrefix(EntityType entityType, String columnNa if (currentSourceIndex > 0) { String currentSource = columnName.substring(0, currentSourceIndex); String formattedKey = ENTITY_TYPE_COLUMN_AND_SOURCE_LABEL_TRANSLATION_TEMPLATE.formatted(entityType.getName(), currentSource); - return translationService.format(formattedKey) + " — "; + return translationService.format(formattedKey) + " — "; } else { return ""; } diff --git a/src/main/java/org/folio/fqm/service/MigrationService.java b/src/main/java/org/folio/fqm/service/MigrationService.java index 02176d48..bb7f4f2e 100644 --- a/src/main/java/org/folio/fqm/service/MigrationService.java +++ b/src/main/java/org/folio/fqm/service/MigrationService.java @@ -1,23 +1,44 @@ package org.folio.fqm.service; -import java.util.List; - -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.NotImplementedException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Optional; +import java.util.UUID; +import javax.annotation.CheckForNull; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.fql.service.FqlService; +import org.folio.fqm.migration.MigratableQueryInformation; +import org.folio.fqm.migration.MigrationStrategy; +import org.folio.fqm.migration.MigrationStrategyRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +@Log4j2 @Service -@RequiredArgsConstructor +@AllArgsConstructor(onConstructor_ = @Autowired) public class MigrationService { + public static final String VERSION_KEY = "_version"; + + public static final UUID REMOVED_ENTITY_TYPE_ID = UUID.fromString("deadbeef-dead-dead-dead-deaddeadbeef"); + + protected static final String CURRENT_VERSION = "1"; + // TODO: replace this with current version in the future? + protected static final String DEFAULT_VERSION = "0"; + + private final FqlService fqlService; + private final MigrationStrategyRepository migrationStrategyRepository; + private final ObjectMapper objectMapper; + public String getLatestVersion() { - // return 1; - throw new NotImplementedException(); + return CURRENT_VERSION; } - public boolean isMigrationNeeded(String fqlQuery) { - // return true; - throw new NotImplementedException(); + public boolean isMigrationNeeded(@CheckForNull String fqlQuery) { + return !this.getLatestVersion().equals(getVersion(fqlQuery)); } public boolean isMigrationNeeded(MigratableQueryInformation migratableQueryInformation) { @@ -25,9 +46,29 @@ public boolean isMigrationNeeded(MigratableQueryInformation migratableQueryInfor } public MigratableQueryInformation migrate(MigratableQueryInformation migratableQueryInformation) { - // return migratableQueryInformation; - throw new NotImplementedException(); + while (isMigrationNeeded(migratableQueryInformation)) { + for (MigrationStrategy strategy : migrationStrategyRepository.getMigrationStrategies()) { + if (strategy.applies(getVersion(migratableQueryInformation.fqlQuery()))) { + log.info("Applying {} to {}", strategy.getLabel(), migratableQueryInformation); + migratableQueryInformation = strategy.apply(fqlService, migratableQueryInformation); + } + } + } + + return migratableQueryInformation; } - public record MigratableQueryInformation(String entityTypeId, String fqlQuery, List fields) {} + public String getVersion(@CheckForNull String fqlQuery) { + if (fqlQuery == null) { + return DEFAULT_VERSION; + } + try { + return Optional + .ofNullable(((ObjectNode) objectMapper.readTree(fqlQuery)).get(VERSION_KEY)) + .map(JsonNode::asText) + .orElse(DEFAULT_VERSION); + } catch (JsonProcessingException e) { + return DEFAULT_VERSION; + } + } } diff --git a/src/main/resources/entity-types/removed.json5 b/src/main/resources/entity-types/removed.json5 new file mode 100644 index 00000000..1a8a68f4 --- /dev/null +++ b/src/main/resources/entity-types/removed.json5 @@ -0,0 +1,14 @@ +{ + id: 'deadbeef-dead-dead-dead-deaddeadbeef', + name: 'removed', + root: true, + private: true, + sources: [ + { + alias: 'not-used', + type: 'db', + target: '(select 1)', + }, + ], + columns: [], +} diff --git a/src/test/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategyTest.java b/src/test/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategyTest.java new file mode 100644 index 00000000..51bd7bf1 --- /dev/null +++ b/src/test/java/org/folio/fqm/migration/AbstractSimpleMigrationStrategyTest.java @@ -0,0 +1,354 @@ +package org.folio.fqm.migration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.folio.fql.service.FqlService; +import org.folio.fqm.migration.warnings.DeprecatedEntityWarning; +import org.folio.fqm.migration.warnings.DeprecatedFieldWarning; +import org.folio.fqm.migration.warnings.EntityTypeWarning; +import org.folio.fqm.migration.warnings.FieldWarning; +import org.folio.fqm.migration.warnings.QueryBreakingWarning; +import org.folio.fqm.migration.warnings.RemovedEntityWarning; +import org.folio.fqm.migration.warnings.RemovedFieldWarning; +import org.folio.fqm.migration.warnings.Warning; +import org.folio.fqm.service.MigrationService; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class AbstractSimpleMigrationStrategyTest { + + FqlService fqlService = new FqlService(); + ObjectMapper objectMapper = new ObjectMapper(); + + // A -> B, field changes + static final UUID UUID_A = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + static final UUID UUID_B = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + // C -> D, no field changes + static final UUID UUID_C = UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc"); + static final UUID UUID_D = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"); + // E has field changes + static final UUID UUID_E = UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); + // F has field changes with wildcard + static final UUID UUID_F = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"); + // 0 has no changes + static final UUID UUID_0 = UUID.fromString("00000000-0000-0000-0000-000000000000"); + + // deprecated ET + static final UUID UUID_0A = UUID.fromString("00000000-0000-0000-0000-aaaaaaaaaaaa"); + // removed ET + static final UUID UUID_0B = UUID.fromString("00000000-0000-0000-0000-bbbbbbbbbbbb"); + // field warnings + static final UUID UUID_0C = UUID.fromString("00000000-0000-0000-0000-cccccccccccc"); + + static class Impl extends AbstractSimpleMigrationStrategy { + + @Override + public String getLabel() { + return "label"; + } + + @Override + public String getSourceVersion() { + return "source"; + } + + @Override + public String getTargetVersion() { + return "target"; + } + + @Override + public Map getEntityTypeChanges() { + return Map.ofEntries(Map.entry(UUID_A, UUID_B), Map.entry(UUID_C, UUID_D)); + } + + @Override + public Map> getFieldChanges() { + return Map.ofEntries( + Map.entry(UUID_A, Map.of("foo", "bar")), + Map.entry(UUID_E, Map.of("foo", "bar")), + Map.entry(UUID_F, Map.of("*", "bar.%s")) + ); + } + + @Override + public Map> getEntityTypeWarnings() { + return Map.ofEntries( + Map.entry(UUID_0A, fql -> new DeprecatedEntityWarning("0a", null)), + Map.entry(UUID_0B, fql -> new RemovedEntityWarning("0b", null, fql)) + ); + } + + @Override + public Map>> getFieldWarnings() { + return Map.ofEntries( + Map.entry( + UUID_0C, + Map.ofEntries( + Map.entry("deprecated", DeprecatedFieldWarning::new), + Map.entry("query_breaking", (field, fql) -> new QueryBreakingWarning(field, "alt", fql)), + Map.entry("removed", (field, fql) -> new RemovedFieldWarning(field, "alt", fql)) + ) + ) + ); + } + } + + static List sourcesWithShouldApply() { + return List.of( + Arguments.of("", false), + Arguments.of("0", false), + Arguments.of("1", false), + Arguments.of("-1", false), + Arguments.of("source", true) + ); + } + + @ParameterizedTest(name = "{0} applies={1}") + @MethodSource("sourcesWithShouldApply") + void testAppliesToMatchingVersions(String version, boolean shouldApply) { + assertThat(new Impl().applies(version), is(shouldApply)); + } + + static List sourcesForMigrationResults() { + return List.of( + // ET change, no FQL changes + Arguments.of( + new MigratableQueryInformation( + UUID_A, + "{\"_version\":\"source\",\"test\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_B, + "{\"_version\":\"target\",\"test\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "bar", "also_unrelated") + ) + ), + // ET change and FQL changes + Arguments.of( + new MigratableQueryInformation( + UUID_A, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_B, + "{\"_version\":\"target\",\"bar\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "bar", "also_unrelated") + ) + ), + // ET change and no complex FQL changes + Arguments.of( + new MigratableQueryInformation( + UUID_A, + """ + {"_version":"source","$and":[ + {"field1": {"$eq": true}}, + {"field2": {"$lte": 3}} + ]} + """, + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_B, + """ + {"_version":"target","$and":[ + {"field1": {"$eq": true}}, + {"field2": {"$lte": 3}} + ]} + """, + List.of("unrelated", "bar", "also_unrelated") + ) + ), + // ET change and complex FQL field change + Arguments.of( + new MigratableQueryInformation( + UUID_A, + """ + {"_version":"source","$and":[ + {"field1": {"$eq": true}}, + {"$and": [ + {"field2": {"$gte": 2}}, + {"foo": {"$eq": "aaa"}} + ]}, + {"field3": {"$lte": 3}} + ]} + """, + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_B, + """ + {"_version":"target","$and":[ + {"field1": {"$eq": true}}, + {"$and": [ + {"field2": {"$gte": 2}}, + {"bar": {"$eq": "aaa"}} + ]}, + {"field3": {"$lte": 3}} + ]} + """, + List.of("unrelated", "bar", "also_unrelated") + ) + ), + // ET change, no FQL changes + Arguments.of( + new MigratableQueryInformation( + UUID_C, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_D, + "{\"_version\":\"target\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "foo", "also_unrelated") + ) + ), + // No ET change, FQL changes + Arguments.of( + new MigratableQueryInformation( + UUID_E, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "foo", "also_unrelated") + ), + new MigratableQueryInformation( + UUID_E, + "{\"_version\":\"target\",\"bar\":{\"$eq\":\"foo\"}}", + List.of("unrelated", "bar", "also_unrelated") + ) + ), + // No ET change, FQL changes (wildcard) + Arguments.of( + new MigratableQueryInformation( + UUID_F, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2") + ), + new MigratableQueryInformation( + UUID_F, + "{\"_version\":\"target\",\"bar.foo\":{\"$eq\":\"foo\"}}", + List.of("bar.field1", "bar.foo", "bar.field2") + ) + ), + // No changes + Arguments.of( + new MigratableQueryInformation( + UUID_0, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2") + ), + new MigratableQueryInformation( + UUID_0, + "{\"_version\":\"target\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2") + ) + ), + // Deprecated ET + Arguments.of( + new MigratableQueryInformation( + UUID_0A, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2") + ), + new MigratableQueryInformation( + UUID_0A, + "{\"_version\":\"target\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2"), + List.of(new DeprecatedEntityWarning("0a", null)) + ) + ), + // Removed ET + Arguments.of( + new MigratableQueryInformation( + UUID_0B, + "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}", + List.of("field1", "foo", "field2") + ), + new MigratableQueryInformation( + MigrationService.REMOVED_ENTITY_TYPE_ID, + "{\"_version\":\"target\"}", + List.of(), + List.of(new RemovedEntityWarning("0b", null, "{\"_version\":\"source\",\"foo\":{\"$eq\":\"foo\"}}")) + ) + ), + // field-level warnings + Arguments.of( + new MigratableQueryInformation( + UUID_0C, + """ + {"_version":"source","$and":[ + {"unrelated": {"$eq": true}}, + {"removed": {"$lte": 3}}, + {"deprecated": {"$lte": 2}}, + {"query_breaking": {"$ne": 2}} + ]} + """, + List.of("unrelated", "removed", "deprecated", "query_breaking") + ), + new MigratableQueryInformation( + UUID_0C, + """ + {"_version":"target","$and":[ + {"unrelated": {"$eq": true}}, + {"deprecated": {"$lte": 2}} + ]} + """, + List.of("unrelated", "deprecated", "query_breaking"), + List.of( + new DeprecatedFieldWarning("deprecated", "{\n \"$lte\" : 2\n}"), + new DeprecatedFieldWarning("deprecated", null), + new QueryBreakingWarning("query_breaking", "alt", "{\n \"$ne\" : 2\n}"), + new RemovedFieldWarning("removed", "alt", "{\n \"$lte\" : 3\n}"), + new RemovedFieldWarning("removed", "alt", null) + ) + ) + ), + // removed top-level query field + Arguments.of( + new MigratableQueryInformation( + UUID_0C, + """ + { + "_version":"source", + "query_breaking": {"$ne": 2} + } + """, + List.of("query_breaking") + ), + new MigratableQueryInformation( + UUID_0C, + "{\"_version\":\"target\"}", + List.of("query_breaking"), + List.of(new QueryBreakingWarning("query_breaking", "alt", "{\n \"$ne\" : 2\n}")) + ) + ) + ); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("sourcesForMigrationResults") + void testMigrationResults(MigratableQueryInformation source, MigratableQueryInformation expected) + throws JsonProcessingException { + MigratableQueryInformation result = new Impl().apply(fqlService, source); + + assertThat(result.entityTypeId(), is(expected.entityTypeId())); + // deserialize to help prevent whitespace/etc breaking the test + assertThat(objectMapper.readTree(result.fqlQuery()), is(objectMapper.readTree(expected.fqlQuery()))); + assertThat(result.fields(), is(expected.fields())); + for (Warning warning : result.warnings()) { + assertThat(warning.toString() + " is expected", expected.warnings().stream().anyMatch(warning::equals), is(true)); + } + assertThat(result.warnings(), hasSize(expected.warnings().size())); + } +} diff --git a/src/test/java/org/folio/fqm/migration/MigrationStrategyRepositoryTest.java b/src/test/java/org/folio/fqm/migration/MigrationStrategyRepositoryTest.java new file mode 100644 index 00000000..2909507d --- /dev/null +++ b/src/test/java/org/folio/fqm/migration/MigrationStrategyRepositoryTest.java @@ -0,0 +1,105 @@ +package org.folio.fqm.migration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.UUID; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.tuple.Pair; +import org.folio.fql.service.FqlService; +import org.folio.fqm.service.MigrationService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@Log4j2 +@TestInstance(Lifecycle.PER_CLASS) // needed to have non-static Arguments +class MigrationStrategyRepositoryTest { + + FqlService fqlService = new FqlService(); + MigrationStrategyRepository migrationStrategyRepository = new MigrationStrategyRepository(); + MigrationService migrationService = new MigrationService(null, null, new ObjectMapper()); + + @Test + void testHasStrategies() { + assertThat(migrationStrategyRepository.getMigrationStrategies(), is(not(empty()))); + } + + List migrationStrategiesAndQueries() { + List strategies = migrationStrategyRepository.getMigrationStrategies(); + List> queries = List.of( + Pair.of("null FQL", new MigratableQueryInformation(new UUID(0, 0), null, List.of())), + Pair.of("empty FQL", new MigratableQueryInformation(new UUID(0, 0), "{}", List.of())), + Pair.of( + "FQL without version", + new MigratableQueryInformation(new UUID(0, 0), "{\"test\":{\"$eq\":\"foo\"}}", List.of()) + ), + Pair.of( + "FQL with invalid operator", + new MigratableQueryInformation(new UUID(0, 0), "{\"test\":{\"$i_am_invalid\":[]}}", List.of()) + ), + Pair.of( + "FQL with version=1 only", + new MigratableQueryInformation(new UUID(0, 0), "{\"_version\":\"1\"}", List.of()) + ), + Pair.of( + "FQL with weird version=-1", + new MigratableQueryInformation(new UUID(0, 0), "{\"_version\":\"-1\"}", List.of()) + ) + ); + + return strategies + .stream() + .flatMap(strategy -> + queries + .stream() + .map(query -> + Arguments.of("%s: %s".formatted(query.getLeft(), strategy.getLabel()), strategy, query.getValue()) + ) + ) + .toList(); + } + + // Tests .applies automatically for all strategies, with an emphasis on weird FQL. + // Note that .apply, where the work is done, will ONLY be tested here if there's a FQL query above + // that matches the migration strategy. This is intentional, to encourage specific testing of the + // migration logic with a relevant query, rather than just getting coverage. + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("migrationStrategiesAndQueries") + void testStrategies(String label, MigrationStrategy strategy, MigratableQueryInformation query) { + boolean applies = strategy.applies(migrationService.getVersion(query.fqlQuery())); + + log.info("{} applies={}", label, applies); + + if (applies) { + strategy.apply(fqlService, query); + } + + // migration application is thoroughly tested for this shared logic + // these test the maps/etc all are set up correctly + if (strategy instanceof AbstractSimpleMigrationStrategy abstractStrategy) { + abstractStrategy.getEntityTypeChanges(); + abstractStrategy.getFieldChanges(); + abstractStrategy + .getEntityTypeWarnings() + .forEach((k, v) -> { + assertThat(v.apply("{}"), is(notNullValue())); + }); + abstractStrategy + .getFieldWarnings() + .forEach((k, v) -> + v.forEach((k2, v2) -> { + assertThat(v2.apply("field", "{}"), is(notNullValue())); + }) + ); + } + } +} diff --git a/src/test/java/org/folio/fqm/migration/strategies/TestTemplate.java b/src/test/java/org/folio/fqm/migration/strategies/TestTemplate.java new file mode 100644 index 00000000..b308bcdb --- /dev/null +++ b/src/test/java/org/folio/fqm/migration/strategies/TestTemplate.java @@ -0,0 +1,48 @@ +package org.folio.fqm.migration.strategies; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.folio.fql.service.FqlService; +import org.folio.fqm.migration.MigratableQueryInformation; +import org.folio.fqm.migration.MigrationStrategy; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@TestInstance(Lifecycle.PER_CLASS) // needed to have non-static Arguments +public abstract class TestTemplate { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public abstract MigrationStrategy getStrategy(); + + /** [Description, Source, Expected] */ + public abstract List getExpectedTransformations(); + + // Tests .applies automatically for all strategies, with an emphasis on weird FQL. + // Note that .apply, where the work is done, will ONLY be tested here if there's a FQL query above + // that matches the migration strategy. This is intentional, to encourage specific testing of the + // migration logic with a relevant query, rather than just getting coverage. + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("getExpectedTransformations") + void testStrategy(String label, MigratableQueryInformation source, MigratableQueryInformation expected) + throws JsonProcessingException { + MigrationStrategy strategy = getStrategy(); + + MigratableQueryInformation actual = strategy.apply(new FqlService(), source); + + assertThat("[ET ID] " + label, actual.entityTypeId(), is(expected.entityTypeId())); + assertThat( + "[FQL] " + label, + objectMapper.readTree(actual.fqlQuery()), + is(objectMapper.readTree(expected.fqlQuery())) + ); + assertThat("[Fields] " + label, actual.fields(), is(expected.fields())); + } +} diff --git a/src/test/java/org/folio/fqm/migration/strategies/V0POCMigrationTest.java b/src/test/java/org/folio/fqm/migration/strategies/V0POCMigrationTest.java new file mode 100644 index 00000000..9e1cf83c --- /dev/null +++ b/src/test/java/org/folio/fqm/migration/strategies/V0POCMigrationTest.java @@ -0,0 +1,182 @@ +package org.folio.fqm.migration.strategies; + +import java.util.List; +import java.util.UUID; +import org.folio.fqm.migration.MigratableQueryInformation; +import org.folio.fqm.migration.MigrationStrategy; +import org.junit.jupiter.params.provider.Arguments; + +class V0POCMigrationTest extends TestTemplate { + + @Override + public MigrationStrategy getStrategy() { + return new V0POCMigration(); + } + + @Override + public List getExpectedTransformations() { + return List.of( + Arguments.of( + "Canned list: Missing Items List", + MigratableQueryInformation + .builder() + .entityTypeId(UUID.fromString("0cb79a4c-f7eb-4941-a104-745224ae0292")) + .fqlQuery( + "{\"item_status\": {\"$in\": [\"missing\", \"aged to lost\", \"claimed returned\", \"declared lost\", \"long missing\" ] }}" + ) + .fields( + List.of( + "id", + "item_hrid", + "holdings_id", + "item_effective_call_number", + "item_effective_call_number_typeid", + "item_effective_call_number_type_name", + "item_holdings_record_id", + "item_status", + "item_copy_number", + "item_barcode", + "item_created_date", + "item_updated_date", + "item_effective_location_id", + "item_effective_location_name", + "item_effective_library_id", + "item_effective_library_name", + "item_effective_library_code", + "item_material_type_id", + "item_material_type", + "instance_id", + "instance_title", + "instance_created_date", + "instance_updated_date", + "instance_primary_contributor", + "item_level_call_number", + "item_level_call_number_typeid", + "item_permanent_location_id", + "item_temporary_location_id", + "item_level_call_number_type_name", + "item_permanent_location_name", + "item_temporary_location_name" + ) + ) + .build(), + MigratableQueryInformation + .builder() + .entityTypeId(UUID.fromString("d0213d22-32cf-490f-9196-d81c3c66e53f")) + .fqlQuery( + "{\"_version\":\"1\",\"items.status_name\":{\"$in\":[\"missing\",\"aged to lost\",\"claimed returned\",\"declared lost\",\"long missing\"]}}" + ) + .fields( + List.of( + "items.id", + "items.hrid", + "holdings.id", + "items.effective_call_number", + "effective_call_number.id", + "effective_call_number.name", + "items.status_name", + "items.copy_number", + "items.barcode", + "items.created_date", + "items.updated_date", + "effective_location.id", + "effective_location.name", + "loclibrary.id", + "loclibrary.name", + "loclibrary.code", + "mtypes.id", + "mtypes.name", + "instances.id", + "instances.title", + "instances.created_at", + "instances.updated_at", + "instances.contributors", + "items.item_level_call_number", + "item_level_call_number.id", + "permanent_location.id", + "temporary_location.id", + "item_level_call_number.name", + "permanent_location.name", + "temporary_location.name" + ) + ) + .build() + ), + Arguments.of( + "Canned list: Expired Patron Loan List", + MigratableQueryInformation + .builder() + .entityTypeId(UUID.fromString("4e09d89a-44ed-418e-a9cc-820dfb27bf3a")) + .fqlQuery("{\"$and\": [{\"loan_status\" : {\"$eq\": \"Open\"}}, {\"user_active\" : {\"$eq\": \"false\"}}]}") + .fields( + List.of( + "user_id", + "user_first_name", + "user_last_name", + "user_full_name", + "user_active", + "user_barcode", + "user_expiration_date", + "user_patron_group_id", + "user_patron_group", + "id", + "loan_status", + "loan_checkout_date", + "loan_due_date", + "loan_policy_id", + "loan_policy_name", + "loan_checkout_servicepoint_id", + "loan_checkout_servicepoint_name", + "item_holdingsrecord_id", + "instance_id", + "instance_title", + "instance_primary_contributor", + "item_id", + "item_barcode", + "item_status", + "item_material_type_id", + "item_material_type" + ) + ) + .build(), + MigratableQueryInformation + .builder() + .entityTypeId(UUID.fromString("d6729885-f2fb-4dc7-b7d0-a865a7f461e4")) + .fqlQuery( + "{\"_version\":\"1\",\"$and\":[{\"loans.status_name\":{\"$eq\":\"Open\"}},{\"users.active\":{\"$eq\":\"false\"}}]}" + ) + .fields( + List.of( + "users.id", + "users.first_name", + "users.last_name", + "users.last_name_first_name", + "users.active", + "users.barcode", + "users.expiration_date", + "groups.id", + "groups.group", + "loans.id", + "loans.status_name", + "loans.loan_date", + "loans.due_date", + "lpolicy.id", + "lpolicy.name", + "cospi.id", + "cospi.name", + "items.hrid", + "instance.id", + "instance.title", + "instance.contributors", + "items.id", + "items.barcode", + "items.status_name", + "mtypes.id", + "mtypes.name" + ) + ) + .build() + ) + ); + } +} diff --git a/src/test/java/org/folio/fqm/migration/warnings/WarningTest.java b/src/test/java/org/folio/fqm/migration/warnings/WarningTest.java new file mode 100644 index 00000000..6c270a5a --- /dev/null +++ b/src/test/java/org/folio/fqm/migration/warnings/WarningTest.java @@ -0,0 +1,116 @@ +package org.folio.fqm.migration.warnings; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import org.folio.fqm.migration.warnings.Warning.WarningType; +import org.folio.spring.i18n.service.TranslationService; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class WarningTest { + + @Mock + TranslationService translationService; + + @Captor + ArgumentCaptor varargCaptor; + + public static List getExpectedTypes() { + return List.of( + Arguments.of(new DeprecatedEntityWarning("old", "new"), WarningType.DEPRECATED_ENTITY), + Arguments.of(new DeprecatedFieldWarning("old", "{}"), WarningType.DEPRECATED_FIELD), + Arguments.of(new QueryBreakingWarning("old", "new", "{}"), WarningType.QUERY_BREAKING), + Arguments.of(new RemovedEntityWarning("old", "new", "{}"), WarningType.REMOVED_ENTITY), + Arguments.of(new RemovedFieldWarning("old", "new", "{}"), WarningType.REMOVED_FIELD) + ); + } + + @ParameterizedTest(name = "{index}: {1}") + @MethodSource("getExpectedTypes") + void testTypes(Warning warning, WarningType expectedType) { + assertThat(warning.getType(), is(expectedType)); + } + + public static List getExpectedTranslations() { + return List.of( + Arguments.of( + new DeprecatedEntityWarning("old", "alt"), + "mod-fqm-manager.migration.warning.DEPRECATED_ENTITY.withAlternative", + List.of("name", "old", "alternative", "alt") + ), + Arguments.of( + new DeprecatedEntityWarning("old", null), + "mod-fqm-manager.migration.warning.DEPRECATED_ENTITY.withoutAlternative", + List.of("name", "old") + ), + Arguments.of( + new DeprecatedFieldWarning("old", null), + "mod-fqm-manager.migration.warning.DEPRECATED_FIELD.field", + List.of("name", "old") + ), + Arguments.of( + new DeprecatedFieldWarning("old", "{}"), + "mod-fqm-manager.migration.warning.DEPRECATED_FIELD.query", + List.of("name", "old") + ), + Arguments.of( + new QueryBreakingWarning("old", "alt", "{}"), + "mod-fqm-manager.migration.warning.QUERY_BREAKING.withAlternative", + List.of("name", "old", "alternative", "alt", "fql", "{}") + ), + Arguments.of( + new QueryBreakingWarning("old", null, "{}"), + "mod-fqm-manager.migration.warning.QUERY_BREAKING.withoutAlternative", + List.of("name", "old", "fql", "{}") + ), + Arguments.of( + new RemovedEntityWarning("old", "alt", "{}"), + "mod-fqm-manager.migration.warning.REMOVED_ENTITY.withAlternative", + List.of("name", "old", "alternative", "alt", "fql", "{}") + ), + Arguments.of( + new RemovedEntityWarning("old", null, "{}"), + "mod-fqm-manager.migration.warning.REMOVED_ENTITY.withoutAlternative", + List.of("name", "old", "fql", "{}") + ), + Arguments.of( + new RemovedFieldWarning("old", "alt", "{}"), + "mod-fqm-manager.migration.warning.REMOVED_FIELD.withAlternative", + List.of("name", "old", "alternative", "alt", "fql", "{}") + ), + Arguments.of( + new RemovedFieldWarning("old", null, "{}"), + "mod-fqm-manager.migration.warning.REMOVED_FIELD.withoutAlternative", + List.of("name", "old", "fql", "{}") + ) + ); + } + + @ParameterizedTest(name = "{index}: {1}") + @MethodSource("getExpectedTranslations") + void testTranslations(Warning warning, String expectedKey, List expectedArgs) { + when(translationService.format(eq(expectedKey), any(Object[].class))).thenReturn("formatted warning"); + + assertThat(warning.getDescription(translationService), is("formatted warning")); + + verify(translationService, times(1)).format(eq(expectedKey), varargCaptor.capture()); + assertThat(Arrays.stream(varargCaptor.getValue()).map(String.class::cast).toList(), is(expectedArgs)); + verifyNoMoreInteractions(translationService); + } +} diff --git a/src/test/java/org/folio/fqm/repository/ResultSetRepositoryTest.java b/src/test/java/org/folio/fqm/repository/ResultSetRepositoryTest.java index f06eb35e..85a11c6c 100644 --- a/src/test/java/org/folio/fqm/repository/ResultSetRepositoryTest.java +++ b/src/test/java/org/folio/fqm/repository/ResultSetRepositoryTest.java @@ -89,7 +89,7 @@ void shouldRunSynchronousQueryAndReturnContents() { UUID entityTypeId = UUID.randomUUID(); List afterId = List.of(UUID.randomUUID().toString()); int limit = 100; - Fql fql = new Fql(new EqualsCondition(new FqlField("key1"), "value1")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("key1"), "value1")); List fields = List.of("id", "key1"); List> expectedFullList = ResultSetRepositoryTestDataProvider.TEST_ENTITY_CONTENTS; // Since we are only asking for "id" and "key1" fields, create expected list without key2 included @@ -111,7 +111,7 @@ void shouldReturnEmptyResultSetForSynchronousQueryWithEmptyFields() { UUID entityTypeId = UUID.randomUUID(); List afterId = List.of(UUID.randomUUID().toString()); int limit = 100; - Fql fql = new Fql(new EqualsCondition(new FqlField("key1"), "value1")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("key1"), "value1")); List fields = List.of(); List> expectedList = List.of(); List> actualList = repo.getResultSet(entityTypeId, fql, fields, afterId, limit); @@ -123,7 +123,7 @@ void shouldReturnEmptyResultSetForSynchronousQueryWithNullFields() { UUID entityTypeId = UUID.randomUUID(); List afterId = List.of(UUID.randomUUID().toString()); int limit = 100; - Fql fql = new Fql(new EqualsCondition(new FqlField("key1"), "value1")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("key1"), "value1")); List> expectedList = List.of(); List> actualList = repo.getResultSet(entityTypeId, fql, null, afterId, limit); assertEquals(expectedList, actualList); @@ -133,7 +133,7 @@ void shouldReturnEmptyResultSetForSynchronousQueryWithNullFields() { void shouldRunSynchronousQueryAndHandleNullAfterIdParameter() { UUID entityTypeId = UUID.randomUUID(); int limit = 100; - Fql fql = new Fql(new EqualsCondition(new FqlField("key1"), "value1")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("key1"), "value1")); List fields = List.of("id", "key1", "key2"); when(entityTypeFlatteningService.getFlattenedEntityType(entityTypeId)) .thenReturn(ResultSetRepositoryTestDataProvider.ENTITY_TYPE); diff --git a/src/test/java/org/folio/fqm/service/MigrationServiceTest.java b/src/test/java/org/folio/fqm/service/MigrationServiceTest.java new file mode 100644 index 00000000..862e6778 --- /dev/null +++ b/src/test/java/org/folio/fqm/service/MigrationServiceTest.java @@ -0,0 +1,192 @@ +package org.folio.fqm.service; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.folio.fql.service.FqlService; +import org.folio.fqm.migration.MigratableQueryInformation; +import org.folio.fqm.migration.MigrationStrategy; +import org.folio.fqm.migration.MigrationStrategyRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MigrationServiceTest { + + @Mock + private FqlService fqlService; + + @Mock + private MigrationStrategyRepository migrationStrategyRepository; + + @Spy + private ObjectMapper objectMapper; + + @InjectMocks + private MigrationService migrationService; + + static List queriesWithExpectedVersions() { + return List.of( + Arguments.of(null, "0"), + Arguments.of("{}", "0"), + Arguments.of("This is Jason, not JSON", "0"), + Arguments.of("{\"test\":{\"$eq\":\"foo\"}}", "0"), + Arguments.of("{\"test\":{\"$i_am_invalid\":[]}}", "0"), + Arguments.of("{\"_version\":\"0\"}", "0"), + Arguments.of("{\"_version\":\"-1\"}", "-1"), + Arguments.of("{\"_version\":\"sauce\"}", "sauce"), + Arguments.of("{\"_version\":\"source-2\"}", "source-2"), + Arguments.of("{\"_version\":\"target\"}", "target"), + Arguments.of("{\"_version\":\"source\"}", "source"), + Arguments.of("{\"_version\":\"source\",\"test\":{\"$eq\":\"foo\"}}", "source") + ); + } + + @ParameterizedTest(name = "{0} has version={1}") + @MethodSource("queriesWithExpectedVersions") + void testAppliesToMatchingVersions(String fql, String expectedVersion) { + assertThat(migrationService.getVersion(fql), is(expectedVersion)); + } + + @ParameterizedTest(name = "{0} with version={1} may need migration") + @MethodSource("queriesWithExpectedVersions") + void testIsMigrationNeeded(String fql, String version) { + assertThat( + migrationService.isMigrationNeeded(MigratableQueryInformation.builder().fqlQuery(fql).build()), + is(not(version.equals(migrationService.getLatestVersion()))) + ); + } + + @ParameterizedTest(name = "{0} with version={1} may need migration") + @MethodSource("queriesWithExpectedVersions") + void testMigrateDoesNothingForUpToDate(String fql, String version) { + if (version.equals(migrationService.getLatestVersion())) { + assertThat( + migrationService.migrate(MigratableQueryInformation.builder().fqlQuery(fql).build()).fqlQuery(), + is(fql) + ); + verifyNoInteractions(migrationStrategyRepository); + } + } + + @Test + void testMigrationWorks() { + String fql = "{\"_version\":\"source\",\"test\":{\"$eq\":\"foo\"}}"; + + MigrationStrategy migrationStrategy = spy(new TestMigrationStrategy(true, 1)); + + when(migrationStrategyRepository.getMigrationStrategies()).thenReturn(List.of(migrationStrategy)); + + assertThat( + migrationService.migrate(MigratableQueryInformation.builder().fqlQuery(fql).build()).fqlQuery(), + is( + MigratableQueryInformation + .builder() + .fqlQuery(fql.replace("source", migrationService.getLatestVersion())) + .build() + .fqlQuery() + ) + ); + verify(migrationStrategy, times(1)).apply(fqlService, MigratableQueryInformation.builder().fqlQuery(fql).build()); + } + + @Test + void testMigrationWorksWithMultipleIterations() { + String fql = "{\"_version\":\"source\",\"test\":{\"$eq\":\"foo\"}}"; + + MigrationStrategy migrationStrategy = spy(new TestMigrationStrategy(true, 3)); + + when(migrationStrategyRepository.getMigrationStrategies()).thenReturn(List.of(migrationStrategy)); + + assertThat( + migrationService.migrate(MigratableQueryInformation.builder().fqlQuery(fql).build()).fqlQuery(), + is( + MigratableQueryInformation + .builder() + .fqlQuery(fql.replace("source", migrationService.getLatestVersion())) + .build() + .fqlQuery() + ) + ); + verify(migrationStrategy, times(3)).apply(fqlService, MigratableQueryInformation.builder().fqlQuery(fql).build()); + } + + @Test + void testMigrationOnlyAppliesApplicable() { + String fql = "{\"_version\":\"source\",\"test\":{\"$eq\":\"foo\"}}"; + + MigrationStrategy migrationStrategyApplicable = spy(new TestMigrationStrategy(true, 1)); + MigrationStrategy migrationStrategyInapplicable = spy(new TestMigrationStrategy(false, 0)); + + when(migrationStrategyRepository.getMigrationStrategies()) + .thenReturn(List.of(migrationStrategyInapplicable, migrationStrategyApplicable, migrationStrategyInapplicable)); + + assertThat( + migrationService.migrate(MigratableQueryInformation.builder().fqlQuery(fql).build()).fqlQuery(), + is( + MigratableQueryInformation + .builder() + .fqlQuery(fql.replace("source", migrationService.getLatestVersion())) + .build() + .fqlQuery() + ) + ); + verify(migrationStrategyApplicable, times(1)) + .apply(fqlService, MigratableQueryInformation.builder().fqlQuery(fql).build()); + verify(migrationStrategyApplicable, times(1)).getLabel(); + verify(migrationStrategyApplicable, times(1)).applies(anyString()); + verify(migrationStrategyInapplicable, times(2)).applies(anyString()); + + verifyNoMoreInteractions(migrationStrategyApplicable, migrationStrategyInapplicable); + } + + @RequiredArgsConstructor + private class TestMigrationStrategy implements MigrationStrategy { + + final boolean applies; + final int requiredCount; + int count = 0; + + @Override + public boolean applies(String version) { + return applies; + } + + @Override + public MigratableQueryInformation apply( + FqlService fqlService, + MigratableQueryInformation migratableQueryInformation + ) { + if (++count != requiredCount) { + return migratableQueryInformation; + } + return MigratableQueryInformation + .builder() + .fqlQuery(migratableQueryInformation.fqlQuery().replace("source", migrationService.getLatestVersion())) + .build(); + } + + @Override + public String getLabel() { + return "Test"; + } + } +} diff --git a/src/test/java/org/folio/fqm/service/QueryProcessorServiceTest.java b/src/test/java/org/folio/fqm/service/QueryProcessorServiceTest.java index b712c624..b1e5e7ac 100644 --- a/src/test/java/org/folio/fqm/service/QueryProcessorServiceTest.java +++ b/src/test/java/org/folio/fqm/service/QueryProcessorServiceTest.java @@ -38,7 +38,7 @@ public void setup() { @Test void shouldGetIdsInBatch() { - Fql fql = new Fql(new EqualsCondition(new FqlField("status"), "missing")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("status"), "missing")); String tenantId = "tenant_01"; String fqlCriteria = "{\"status\": {\"$eq\": \"missing\"}}"; EntityType entityType = new EntityType(); @@ -67,7 +67,7 @@ void shouldGetIdsInBatch() { @Test void shouldConsumeButNotThrowError() { - Fql fql = new Fql(new EqualsCondition(new FqlField("status"), "missing")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("status"), "missing")); String tenantId = "tenant_01"; String fqlCriteria = "{\"status\": {\"$eq\": \"missing\"}}"; EntityType entityType = new EntityType(); @@ -106,7 +106,7 @@ void shouldRunSynchronousQueryAndReturnPaginatedResults() { """; List afterId = List.of(UUID.randomUUID().toString()); int limit = 100; - Fql expectedFql = new Fql(new EqualsCondition(new FqlField("status"), "value1")); + Fql expectedFql = new Fql("", new EqualsCondition(new FqlField("status"), "value1")); List fields = List.of("field1", "field2"); List> expectedContent = List.of( Map.of("field1", "value1", "field2", "value2"), diff --git a/src/test/java/org/folio/fqm/utils/IdStreamerTest.java b/src/test/java/org/folio/fqm/utils/IdStreamerTest.java index 4a0a58b6..57279672 100644 --- a/src/test/java/org/folio/fqm/utils/IdStreamerTest.java +++ b/src/test/java/org/folio/fqm/utils/IdStreamerTest.java @@ -69,7 +69,7 @@ void setup() { @Test void shouldFetchIdStreamForFql() { - Fql fql = new Fql(new EqualsCondition(new FqlField("field1"), "value1")); + Fql fql = new Fql("", new EqualsCondition(new FqlField("field1"), "value1")); List> expectedIds = new ArrayList<>(); TEST_CONTENT_IDS.forEach(contentId -> expectedIds.add(List.of(contentId.toString()))); List> actualIds = new ArrayList<>(); diff --git a/translations/mod-fqm-manager/en.json b/translations/mod-fqm-manager/en.json index 48b5294d..09f07b73 100644 --- a/translations/mod-fqm-manager/en.json +++ b/translations/mod-fqm-manager/en.json @@ -14,16 +14,6 @@ "entityType.drv_acquisitions_unit_details.updated_by_user_id": "Updated by user ID", "entityType.drv_acquisitions_unit_details.updated_by_username": "Updated by username", "entityType.drv_acquisitions_unit_details.updated_date": "Updated date", - "entityType.drv_call_number_type_details": "Call number type", - "entityType.drv_call_number_type_details.created_by_user_id": "Created by user ID", - "entityType.drv_call_number_type_details.created_by_username": "Created by username", - "entityType.drv_call_number_type_details.created_date": "Created date", - "entityType.drv_call_number_type_details.id": "ID", - "entityType.drv_call_number_type_details.name": "Name", - "entityType.drv_call_number_type_details.source": "Source", - "entityType.drv_call_number_type_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_call_number_type_details.updated_by_username": "Updated by username", - "entityType.drv_call_number_type_details.updated_date": "Updated date", "entityType.drv_categories_details": "Categories", "entityType.drv_categories_details.created_by_user_id": "Created by user ID", "entityType.drv_categories_details.created_by_username": "Created by username", @@ -71,46 +61,46 @@ "entityType.drv_contributor_type_details.updated_by_user_id": "Updated by user ID", "entityType.drv_contributor_type_details.updated_by_username": "Updated by username", "entityType.drv_contributor_type_details.updated_date": "Updated date", - "entityType.drv_user_custom_field_details": "User Custom field", - "entityType.drv_user_custom_field_details.checkbox_field_default": "Checkbox field default", - "entityType.drv_user_custom_field_details.created_by_user_id": "Created by user ID", - "entityType.drv_user_custom_field_details.created_by_username": "Created by username", - "entityType.drv_user_custom_field_details.created_date": "Created date", - "entityType.drv_user_custom_field_details.entity_type": "Entity type", - "entityType.drv_user_custom_field_details.help_text": "Help text", - "entityType.drv_user_custom_field_details.id": "ID", - "entityType.drv_user_custom_field_details.is_repeatable": "Is repeatable", - "entityType.drv_user_custom_field_details.name": "Name", - "entityType.drv_user_custom_field_details.order": "Order", - "entityType.drv_user_custom_field_details.ref_id": "Ref ID", - "entityType.drv_user_custom_field_details.required": "Required", - "entityType.drv_user_custom_field_details.select_field_multi_select": "Select field multi select", - "entityType.drv_user_custom_field_details.select_field_options_sorting_order": "Select field options sorting order", - "entityType.drv_user_custom_field_details.select_field_options_values": "Select field options values", - "entityType.drv_user_custom_field_details.select_field_options_values.default": "Default", - "entityType.drv_user_custom_field_details.select_field_options_values.default._qualified": "Default", - "entityType.drv_user_custom_field_details.select_field_options_values.select_field_option_value_id": "Select field option value ID", - "entityType.drv_user_custom_field_details.select_field_options_values.select_field_option_value_id._qualified": "Select field option value ID", - "entityType.drv_user_custom_field_details.select_field_options_values.value": "Value", - "entityType.drv_user_custom_field_details.select_field_options_values.value._qualified": "Value", - "entityType.drv_user_custom_field_details.text_field_field_format": "Text field field format", - "entityType.drv_user_custom_field_details.type": "Type", - "entityType.drv_user_custom_field_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_user_custom_field_details.updated_by_username": "Updated by username", - "entityType.drv_user_custom_field_details.updated_date": "Updated date", - "entityType.drv_user_custom_field_details.visible": "Visible", - "entityType.drv_department_details": "Department", - "entityType.drv_department_details.code": "Code", - "entityType.drv_department_details.created_by_user_id": "Created by user ID", - "entityType.drv_department_details.created_by_username": "Created by username", - "entityType.drv_department_details.created_date": "Created date", - "entityType.drv_department_details.id": "ID", - "entityType.drv_department_details.name": "Name", - "entityType.drv_department_details.source": "Source", - "entityType.drv_department_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_department_details.updated_by_username": "Updated by username", - "entityType.drv_department_details.updated_date": "Updated date", - "entityType.drv_department_details.usage_number": "Usage number", + "entityType.simple_user_custom_field_details": "User Custom field", + "entityType.simple_user_custom_field_details.checkbox_field_default": "Checkbox field default", + "entityType.simple_user_custom_field_details.created_by_user_id": "Created by user ID", + "entityType.simple_user_custom_field_details.created_by_username": "Created by username", + "entityType.simple_user_custom_field_details.created_date": "Created date", + "entityType.simple_user_custom_field_details.entity_type": "Entity type", + "entityType.simple_user_custom_field_details.help_text": "Help text", + "entityType.simple_user_custom_field_details.id": "ID", + "entityType.simple_user_custom_field_details.is_repeatable": "Is repeatable", + "entityType.simple_user_custom_field_details.name": "Name", + "entityType.simple_user_custom_field_details.order": "Order", + "entityType.simple_user_custom_field_details.ref_id": "Ref ID", + "entityType.simple_user_custom_field_details.required": "Required", + "entityType.simple_user_custom_field_details.select_field_multi_select": "Select field multi select", + "entityType.simple_user_custom_field_details.select_field_options_sorting_order": "Select field options sorting order", + "entityType.simple_user_custom_field_details.select_field_options_values": "Select field options values", + "entityType.simple_user_custom_field_details.select_field_options_values.default": "Default", + "entityType.simple_user_custom_field_details.select_field_options_values.default._qualified": "Default", + "entityType.simple_user_custom_field_details.select_field_options_values.select_field_option_value_id": "Select field option value ID", + "entityType.simple_user_custom_field_details.select_field_options_values.select_field_option_value_id._qualified": "Select field option value ID", + "entityType.simple_user_custom_field_details.select_field_options_values.value": "Value", + "entityType.simple_user_custom_field_details.select_field_options_values.value._qualified": "Value", + "entityType.simple_user_custom_field_details.text_field_field_format": "Text field field format", + "entityType.simple_user_custom_field_details.type": "Type", + "entityType.simple_user_custom_field_details.updated_by_user_id": "Updated by user ID", + "entityType.simple_user_custom_field_details.updated_by_username": "Updated by username", + "entityType.simple_user_custom_field_details.updated_date": "Updated date", + "entityType.simple_user_custom_field_details.visible": "Visible", + "entityType.simple_department_details": "Department", + "entityType.simple_department_details.code": "Code", + "entityType.simple_department_details.created_by_user_id": "Created by user ID", + "entityType.simple_department_details.created_by_username": "Created by username", + "entityType.simple_department_details.created_date": "Created date", + "entityType.simple_department_details.id": "ID", + "entityType.simple_department_details.name": "Name", + "entityType.simple_department_details.source": "Source", + "entityType.simple_department_details.updated_by_user_id": "Updated by user ID", + "entityType.simple_department_details.updated_by_username": "Updated by username", + "entityType.simple_department_details.updated_date": "Updated date", + "entityType.simple_department_details.usage_number": "Usage number", "entityType.drv_expense_class_details": "Expense class", "entityType.drv_expense_class_details.code": "Code", "entityType.drv_expense_class_details.created_by_user_id": "Created by user ID", @@ -217,25 +207,14 @@ "entityType.composite_holdings_record.effective_library": "Effective library", "entityType.composite_holdings_record.permanent_location": "Permanent location", "entityType.composite_holdings_record.temporary_location": "Temporary location", - "entityType.drv_instance_status_details": "Instance status", - "entityType.drv_instance_status_details.code": "Code", - "entityType.drv_instance_status_details.created_by_user_id": "Created by user ID", - "entityType.drv_instance_status_details.created_by_username": "Created by username", - "entityType.drv_instance_status_details.created_date": "Created date", - "entityType.drv_instance_status_details.id": "ID", - "entityType.drv_instance_status_details.name": "Name", - "entityType.drv_instance_status_details.source": "Source", - "entityType.drv_instance_status_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_instance_status_details.updated_by_username": "Updated by username", - "entityType.drv_instance_status_details.updated_date": "Updated date", "entityType.composite_instances": "Instances", "entityType.composite_instances.instance": "Instance", "entityType.composite_instances.inst_stat": "Instance status", "entityType.composite_instances.mode_of_issuance": "Mode of issuance", "entityType.composite_instances.instance_type": "Resource type", - "entityType.drv_inventory_statistical_code_full": "Statistical code", - "entityType.drv_inventory_statistical_code_full.id": "Statistical code ID", - "entityType.drv_inventory_statistical_code_full.statistical_code": "Statistical code", + "entityType.simple_inventory_statistical_code_full": "Statistical code", + "entityType.simple_inventory_statistical_code_full.id": "Statistical code ID", + "entityType.simple_inventory_statistical_code_full.statistical_code": "Statistical code", "entityType.drv_invoice_lines_details": "Invoice lines", "entityType.drv_invoice_lines_details.account_number": "Account number", "entityType.drv_invoice_lines_details.accounting_code": "Accounting code", @@ -332,54 +311,6 @@ "entityType.drv_invoices_details.vendor_id": "Vendor ID", "entityType.drv_invoices_details.vendor_invoice_no": "Vendor invoice no", "entityType.drv_invoices_details.voucher_number": "Voucher number", - "entityType.drv_item_callnumber_location": "Item, call number and location", - "entityType.drv_item_callnumber_location.id": "Item ID", - "entityType.drv_item_callnumber_location.item_barcode": "Item barcode", - "entityType.drv_item_callnumber_location.item_copy_number": "Item copy number", - "entityType.drv_item_callnumber_location.item_created_date": "Item created date", - "entityType.drv_item_callnumber_location.item_effective_call_number": "Effective call number", - "entityType.drv_item_callnumber_location.item_effective_call_number_type_name": "Effective call number type name", - "entityType.drv_item_callnumber_location.item_effective_call_number_typeid": "Effective call number typeId", - "entityType.drv_item_callnumber_location.item_effective_library_code": "Item effective library code", - "entityType.drv_item_callnumber_location.item_effective_library_id": "Item effective library id", - "entityType.drv_item_callnumber_location.item_effective_library_name": "Item effective library name", - "entityType.drv_item_callnumber_location.item_effective_location_id": "Item location ID", - "entityType.drv_item_callnumber_location.item_effective_location_name": "Item location name", - "entityType.drv_item_callnumber_location.item_holdings_record_id": "Holdings record ID", - "entityType.drv_item_callnumber_location.item_hrid": "Item hrid", - "entityType.drv_item_callnumber_location.item_level_call_number": "Call number", - "entityType.drv_item_callnumber_location.item_level_call_number_type_name": "Call number type name", - "entityType.drv_item_callnumber_location.item_level_call_number_typeid": "Call number typeId", - "entityType.drv_item_callnumber_location.item_material_type": "Material type", - "entityType.drv_item_callnumber_location.item_material_type_id": "Material ID", - "entityType.drv_item_callnumber_location.item_permanent_location_id": "Item permanent ID", - "entityType.drv_item_callnumber_location.item_permanent_location_name": "Item permanent location name", - "entityType.drv_item_callnumber_location.item_status": "Item status", - "entityType.drv_item_callnumber_location.item_updated_date": "Item updated date", - "entityType.drv_item_holdingsrecord_instance": "Items, Holdings records, Instance", - "entityType.drv_item_holdingsrecord_instance.holdings_id": "Holdings ID", - "entityType.drv_item_holdingsrecord_instance.id": "Item ID", - "entityType.drv_item_holdingsrecord_instance.instance_created_date": "Instance created date", - "entityType.drv_item_holdingsrecord_instance.instance_id": "Instance ID", - "entityType.drv_item_holdingsrecord_instance.instance_primary_contributor": "Instance primary contributor", - "entityType.drv_item_holdingsrecord_instance.instance_title": "Instance title", - "entityType.drv_item_holdingsrecord_instance.instance_updated_date": "Instance updated date", - "entityType.drv_item_holdingsrecord_instance.item_barcode": "Item barcode", - "entityType.drv_item_holdingsrecord_instance.item_copy_number": "Item copy number", - "entityType.drv_item_holdingsrecord_instance.item_created_date": "Item created date", - "entityType.drv_item_holdingsrecord_instance.item_effective_call_number": "Item effective call number", - "entityType.drv_item_holdingsrecord_instance.item_effective_call_number_typeid": "Item effective call number type ID", - "entityType.drv_item_holdingsrecord_instance.item_effective_location_id": "Item location ID", - "entityType.drv_item_holdingsrecord_instance.item_hrid": "Item hrid", - "entityType.drv_item_holdingsrecord_instance.item_level_call_number": "Item call number", - "entityType.drv_item_holdingsrecord_instance.item_level_call_number_typeid": "Item call number type ID", - "entityType.drv_item_holdingsrecord_instance.item_material_type_id": "Item material ID", - "entityType.drv_item_holdingsrecord_instance.item_permanent_location_id": "Item permanent ID", - "entityType.drv_item_holdingsrecord_instance.item_status": "Item status", - "entityType.drv_item_holdingsrecord_instance.item_temporary_location_id": "Item temporary ID", - "entityType.drv_item_holdingsrecord_instance.item_updated_date": "Item updated date", - "entityType.drv_item_status": "Item status", - "entityType.drv_item_status.item_status": "Item status", "entityType.drv_ledger_details": "Ledger", "entityType.drv_ledger_details.acq_unit_ids": "Acq unit IDs", "entityType.drv_ledger_details.allocated": "Allocated", @@ -412,52 +343,6 @@ "entityType.drv_ledger_details.updated_by_username": "Updated by username", "entityType.drv_ledger_details.updated_date": "Updated date", "entityType.drv_ledger_details.version": "Version", - "entityType.drv_loan_details": "Loans", - "entityType.drv_loan_details._custom_field_possessive": "Loan's {customField}", - "entityType.drv_loan_details.holdings_id": "Holdings ID", - "entityType.drv_loan_details.id": "Loan ID", - "entityType.drv_loan_details.instance_id": "Instance ID", - "entityType.drv_loan_details.instance_primary_contributor": "Instance primary contributor", - "entityType.drv_loan_details.instance_title": "Instance title", - "entityType.drv_loan_details.item_barcode": "Item barcode", - "entityType.drv_loan_details.item_call_number": "Item call number", - "entityType.drv_loan_details.item_id": "Item ID", - "entityType.drv_loan_details.item_material_type": "Item material type", - "entityType.drv_loan_details.item_material_type_id": "Item material type ID", - "entityType.drv_loan_details.item_status": "Item status", - "entityType.drv_loan_details.loan_checkin_servicepoint_id": "Loan checkin service point ID", - "entityType.drv_loan_details.loan_checkin_servicepoint_name": "Loan checkin service point name", - "entityType.drv_loan_details.loan_checkout_date": "Loan checkout date", - "entityType.drv_loan_details.loan_checkout_servicepoint_id": "Loan checkout service point ID", - "entityType.drv_loan_details.loan_checkout_servicepoint_name": "Loan checkout service point name", - "entityType.drv_loan_details.loan_due_date": "Loan due date", - "entityType.drv_loan_details.loan_policy_id": "Loan policy ID", - "entityType.drv_loan_details.loan_policy_name": "Loan policy name", - "entityType.drv_loan_details.loan_return_date": "Loan return date", - "entityType.drv_loan_details.loan_status": "Loan status", - "entityType.drv_loan_details.user_active": "User active", - "entityType.drv_loan_details.user_barcode": "User barcode", - "entityType.drv_loan_details.user_expiration_date": "User expiration date", - "entityType.drv_loan_details.user_first_name": "User first name", - "entityType.drv_loan_details.user_full_name": "User full name", - "entityType.drv_loan_details.user_id": "User ID", - "entityType.drv_loan_details.user_last_name": "User last name", - "entityType.drv_loan_details.user_patron_group": "User patron group", - "entityType.drv_loan_details.user_patron_group_id": "User patron group ID", - "entityType.drv_loan_status": "Loan status", - "entityType.drv_loan_status.loan_status": "Loan status", - "entityType.drv_location_details": "Locations", - "entityType.drv_location_details.campus_id": "Campus ID", - "entityType.drv_location_details.code": "Code", - "entityType.drv_location_details.description": "Description", - "entityType.drv_location_details.details": "Details", - "entityType.drv_location_details.discovery_display_name": "Discovery display name", - "entityType.drv_location_details.id": "ID", - "entityType.drv_location_details.institution_id": "Institution ID", - "entityType.drv_location_details.is_active": "Is active", - "entityType.drv_location_details.name": "Name", - "entityType.drv_location_details.primary_service_point": "Primary service point", - "entityType.drv_location_details.service_point_ids": "Service point IDs", "entityType.simple_material_type_details": "Material type", "entityType.simple_material_type_details.created_by_user_id": "Created by user UUID", "entityType.simple_material_type_details.jsonb": "JSONB", @@ -469,142 +354,6 @@ "entityType.simple_material_type_details.updated_by_user_id": "Updated by user UUID", "entityType.simple_material_type_details.updated_by_username": "Updated by username", "entityType.simple_material_type_details.updated_date": "Updated date", - "entityType.drv_mode_of_issuance_details": "Mode of issuance", - "entityType.drv_mode_of_issuance_details.created_by_user_id": "Created by user ID", - "entityType.drv_mode_of_issuance_details.created_by_username": "Created by username", - "entityType.drv_mode_of_issuance_details.created_date": "Created date", - "entityType.drv_mode_of_issuance_details.id": "ID", - "entityType.drv_mode_of_issuance_details.name": "Name", - "entityType.drv_mode_of_issuance_details.source": "Source", - "entityType.drv_mode_of_issuance_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_mode_of_issuance_details.updated_by_username": "Updated by username", - "entityType.drv_mode_of_issuance_details.updated_date": "Updated date", - "entityType.drv_organization_contacts": "Organizations — contact info", - "entityType.drv_organization_contacts.accounting_code": "Organization accounting code", - "entityType.drv_organization_contacts.acquisition_unit_id": "Acquisition unit IDs", - "entityType.drv_organization_contacts.acquisition_unit_name": "Acquisition units", - "entityType.drv_organization_contacts.address": "Contact addresses", - "entityType.drv_organization_contacts.address.address_line_1": "Line 1", - "entityType.drv_organization_contacts.address.address_line_1._qualified": "Contact address line 1", - "entityType.drv_organization_contacts.address.address_line_2": "Line 2", - "entityType.drv_organization_contacts.address.address_line_2._qualified": "Contact address line 2", - "entityType.drv_organization_contacts.address.category_ids": "Category IDs", - "entityType.drv_organization_contacts.address.category_ids._qualified": "Address category IDs", - "entityType.drv_organization_contacts.address.category_names": "Categories", - "entityType.drv_organization_contacts.address.category_names._qualified": "Address category names", - "entityType.drv_organization_contacts.address.city": "City", - "entityType.drv_organization_contacts.address.city._qualified": "Contact address city", - "entityType.drv_organization_contacts.address.country": "Country", - "entityType.drv_organization_contacts.address.country._qualified": "Contact address country", - "entityType.drv_organization_contacts.address.postal_code": "Postal code", - "entityType.drv_organization_contacts.address.postal_code._qualified": "Contact address postal code", - "entityType.drv_organization_contacts.address.state_region": "State/province", - "entityType.drv_organization_contacts.address.state_region._qualified": "Contact address state/province", - "entityType.drv_organization_contacts.alias": "Organization alias", - "entityType.drv_organization_contacts.alias.description": "Description", - "entityType.drv_organization_contacts.alias.description._qualified": "Alias description", - "entityType.drv_organization_contacts.alias.value": "Value", - "entityType.drv_organization_contacts.alias.value._qualified": "Alias value", - "entityType.drv_organization_contacts.code": "Organization code", - "entityType.drv_organization_contacts.description": "Organization description", - "entityType.drv_organization_contacts.email": "Contact email addresses", - "entityType.drv_organization_contacts.email.category_ids": "Category IDs", - "entityType.drv_organization_contacts.email.category_ids._qualified": "Email category IDs", - "entityType.drv_organization_contacts.email.category_names": "Categories", - "entityType.drv_organization_contacts.email.category_names._qualified": "Email category names", - "entityType.drv_organization_contacts.email.description": "Description", - "entityType.drv_organization_contacts.email.description._qualified": "Contact email description", - "entityType.drv_organization_contacts.email.email": "Email address", - "entityType.drv_organization_contacts.email.email._qualified": "Contact email address", - "entityType.drv_organization_contacts.id": "Organization ID", - "entityType.drv_organization_contacts.last_updated": "Organization last updated", - "entityType.drv_organization_contacts.name": "Organization name", - "entityType.drv_organization_contacts.organization_status": "Organization status", - "entityType.drv_organization_contacts.organization_type_ids": "Organization type IDs", - "entityType.drv_organization_contacts.organization_type_name": "Organization type names", - "entityType.drv_organization_contacts.phone_number": "Contact phone numbers", - "entityType.drv_organization_contacts.phone_number.category_ids": "Category IDs", - "entityType.drv_organization_contacts.phone_number.category_ids._qualified": "Phone number category IDs", - "entityType.drv_organization_contacts.phone_number.category_names": "Categories", - "entityType.drv_organization_contacts.phone_number.category_names._qualified": "Phone number category names", - "entityType.drv_organization_contacts.phone_number.phone_number": "Phone number", - "entityType.drv_organization_contacts.phone_number.phone_number._qualified": "Contact phone number", - "entityType.drv_organization_contacts.phone_number.type": "Type", - "entityType.drv_organization_contacts.phone_number.type._qualified": "Contact phone number type", - "entityType.drv_organization_contacts.url": "Contact URLs", - "entityType.drv_organization_contacts.url.category_ids": "Category IDs", - "entityType.drv_organization_contacts.url.category_ids._qualified": "URL category IDs", - "entityType.drv_organization_contacts.url.category_names": "Categories", - "entityType.drv_organization_contacts.url.category_names._qualified": "URL category names", - "entityType.drv_organization_contacts.url.description": "Description", - "entityType.drv_organization_contacts.url.description._qualified": "Contact URL description", - "entityType.drv_organization_contacts.url.url": "URL", - "entityType.drv_organization_contacts.url.url._qualified": "Contact URL", - "entityType.drv_organization_details": "Organizations — vendor info", - "entityType.drv_organization_details.accounting_code": "Organization accounting code", - "entityType.drv_organization_details.accounts": "Organization account", - "entityType.drv_organization_details.accounts.accountNo": "Account number", - "entityType.drv_organization_details.accounts.accountNo._qualified": "Account number", - "entityType.drv_organization_details.accounts.accountStatus": "Account status", - "entityType.drv_organization_details.accounts.accountStatus._qualified": "Account status", - "entityType.drv_organization_details.accounts.acqUnitIds": "Acquisition unit ID list", - "entityType.drv_organization_details.accounts.acqUnitIds._qualified": "Account acquisition unit ID list", - "entityType.drv_organization_details.accounts.acquisition_unit": "Acquisition unit list", - "entityType.drv_organization_details.accounts.acquisition_unit._qualified": "Account acquisition unit list", - "entityType.drv_organization_details.accounts.appSystemNo": "Accounting code", - "entityType.drv_organization_details.accounts.appSystemNo._qualified": "Account accounting code", - "entityType.drv_organization_details.accounts.contactInfo": "Contact Info", - "entityType.drv_organization_details.accounts.contactInfo._qualified": "Account contact Info", - "entityType.drv_organization_details.accounts.description": "Description", - "entityType.drv_organization_details.accounts.description._qualified": "Account description", - "entityType.drv_organization_details.accounts.libraryCode": "Library code", - "entityType.drv_organization_details.accounts.libraryCode._qualified": "Account library code", - "entityType.drv_organization_details.accounts.libraryEdiCode": "Library EDI code", - "entityType.drv_organization_details.accounts.libraryEdiCode._qualified": "Account library EDI code", - "entityType.drv_organization_details.accounts.name": "Name", - "entityType.drv_organization_details.accounts.name._qualified": "Account name", - "entityType.drv_organization_details.accounts.notes": "Notes", - "entityType.drv_organization_details.accounts.notes._qualified": "Account notes", - "entityType.drv_organization_details.accounts.paymentMethod": "Payment method", - "entityType.drv_organization_details.accounts.paymentMethod._qualified": "Account payment method", - "entityType.drv_organization_details.acquisition_unit": "Acquisition unit list", - "entityType.drv_organization_details.acqunit_ids": "Acquisition unit ID list", - "entityType.drv_organization_details.agreements": "Organization agreements", - "entityType.drv_organization_details.agreements.discount": "Discount percentage", - "entityType.drv_organization_details.agreements.discount._qualified": "Agreement discount percentage", - "entityType.drv_organization_details.agreements.name": "Vendor terms name", - "entityType.drv_organization_details.agreements.name._qualified": "Agreement vendor terms name", - "entityType.drv_organization_details.agreements.notes": "Notes", - "entityType.drv_organization_details.agreements.notes._qualified": "Agreement notes", - "entityType.drv_organization_details.agreements.referenceUrl": "Reference URL", - "entityType.drv_organization_details.agreements.referenceUrl._qualified": "Agreement reference URL", - "entityType.drv_organization_details.alias": "Organization alias", - "entityType.drv_organization_details.alias.description": "Description", - "entityType.drv_organization_details.alias.description._qualified": "Alias description", - "entityType.drv_organization_details.alias.value": "Value", - "entityType.drv_organization_details.alias.value._qualified": "Alias value", - "entityType.drv_organization_details.claiming_interval": "Organization claiming interval", - "entityType.drv_organization_details.code": "Organization code", - "entityType.drv_organization_details.description": "Organization description", - "entityType.drv_organization_details.discount_percent": "Organization discount percent", - "entityType.drv_organization_details.expected_activation_interval": "Organization expected activation interval", - "entityType.drv_organization_details.expected_invoice_interval": "Organization expected invoice interval", - "entityType.drv_organization_details.expected_receipt_interval": "Organization expected receipt interval", - "entityType.drv_organization_details.export_to_accounting": "Organization export to accounting", - "entityType.drv_organization_details.id": "Organization ID", - "entityType.drv_organization_details.last_updated": "Organization last updated", - "entityType.drv_organization_details.name": "Organization name", - "entityType.drv_organization_details.organization_status": "Organization status", - "entityType.drv_organization_details.organization_type_ids": "Organization type IDs", - "entityType.drv_organization_details.organization_type_name": "Organization type names", - "entityType.drv_organization_details.payment_method": "Organization payment method", - "entityType.drv_organization_details.renewal_activation_interval": "Organization renewal activation interval", - "entityType.drv_organization_details.subscription_interval": "Organization subscription interval", - "entityType.drv_organization_details.tax_id": "Organization tax ID", - "entityType.drv_organization_details.tax_percentage": "Organization tax percentage", - "entityType.drv_organization_details.vendor_currencies": "Organization vendor currencies", - "entityType.drv_pol_currency": "POL currency", - "entityType.drv_pol_currency.currency": "POL currency", "entityType.drv_purchase_order_details": "Purchase order", "entityType.drv_purchase_order_details.acq_unit_ids": "Acquisition unit IDs", "entityType.drv_purchase_order_details.acq_units": "Acquisition unit names", @@ -641,57 +390,6 @@ "entityType.drv_purchase_order_details.updated_date": "Updated date", "entityType.drv_purchase_order_details.vendor": "Vendor", "entityType.drv_purchase_order_details.workflow_status": "Workflow status", - "entityType.drv_purchase_order_line_details": "Purchase order lines", - "entityType.drv_purchase_order_line_details._custom_field_possessive": "Purchase order line's {customField}", - "entityType.drv_purchase_order_line_details.acquisition_unit": "Acquisition unit list", - "entityType.drv_purchase_order_line_details.acqunit_ids": "Acquisition unit ID list", - "entityType.drv_purchase_order_line_details.fund_distribution": "Fund distributions", - "entityType.drv_purchase_order_line_details.fund_distribution.code": "Code", - "entityType.drv_purchase_order_line_details.fund_distribution.code._qualified": "Fund distribution code", - "entityType.drv_purchase_order_line_details.fund_distribution.distribution_type": "Distribution type", - "entityType.drv_purchase_order_line_details.fund_distribution.distribution_type._qualified": "Fund distribution type", - "entityType.drv_purchase_order_line_details.fund_distribution.encumbrance": "Encumbrance", - "entityType.drv_purchase_order_line_details.fund_distribution.encumbrance._qualified": "Fund distribution encumbrance", - "entityType.drv_purchase_order_line_details.fund_distribution.fund_id": "Fund ID", - "entityType.drv_purchase_order_line_details.fund_distribution.fund_id._qualified": "Fund distribution fund ID", - "entityType.drv_purchase_order_line_details.fund_distribution.value": "Value", - "entityType.drv_purchase_order_line_details.fund_distribution.value._qualified": "Fund distribution value", - "entityType.drv_purchase_order_line_details.fund_distribution_code": "Fund distribution code", - "entityType.drv_purchase_order_line_details.fund_distribution_encumbrance": "Fund distribution encumbrance", - "entityType.drv_purchase_order_line_details.fund_distribution_type": "Fund distribution type", - "entityType.drv_purchase_order_line_details.fund_distribution_value": "Fund distribution value", - "entityType.drv_purchase_order_line_details.fund_id": "Fund ID", - "entityType.drv_purchase_order_line_details.id": "POL ID", - "entityType.drv_purchase_order_line_details.po_approved": "PO approved", - "entityType.drv_purchase_order_line_details.po_assigned_to": "PO assigned to", - "entityType.drv_purchase_order_line_details.po_assigned_to_id": "PO assigned to ID", - "entityType.drv_purchase_order_line_details.po_created_by": "PO created by", - "entityType.drv_purchase_order_line_details.po_created_by_id": "PO created by ID", - "entityType.drv_purchase_order_line_details.po_created_date": "PO created date", - "entityType.drv_purchase_order_line_details.po_id": "PO ID", - "entityType.drv_purchase_order_line_details.po_notes": "PO note list", - "entityType.drv_purchase_order_line_details.po_number": "PO number", - "entityType.drv_purchase_order_line_details.po_type": "PO type", - "entityType.drv_purchase_order_line_details.po_updated_by": "PO updated by", - "entityType.drv_purchase_order_line_details.po_updated_by_id": "PO updated by ID", - "entityType.drv_purchase_order_line_details.po_updated_date": "PO updated date", - "entityType.drv_purchase_order_line_details.po_workflow_status": "PO workflow status", - "entityType.drv_purchase_order_line_details.pol_created_by": "POL created by", - "entityType.drv_purchase_order_line_details.pol_created_by_id": "POL created by ID", - "entityType.drv_purchase_order_line_details.pol_created_date": "POL created date", - "entityType.drv_purchase_order_line_details.pol_currency": "POL currency", - "entityType.drv_purchase_order_line_details.pol_description": "POL description", - "entityType.drv_purchase_order_line_details.pol_estimated_price": "POL estimated price", - "entityType.drv_purchase_order_line_details.pol_exchange_rate": "POL exchange rate", - "entityType.drv_purchase_order_line_details.pol_number": "POL number", - "entityType.drv_purchase_order_line_details.pol_payment_status": "POL payment status", - "entityType.drv_purchase_order_line_details.pol_receipt_status": "POL receipt status", - "entityType.drv_purchase_order_line_details.pol_updated_by": "POL updated by", - "entityType.drv_purchase_order_line_details.pol_updated_by_id": "POL updated by ID", - "entityType.drv_purchase_order_line_details.pol_updated_date": "POL updated date", - "entityType.drv_purchase_order_line_details.vendor_code": "Vendor code", - "entityType.drv_purchase_order_line_details.vendor_id": "Vendor ID", - "entityType.drv_purchase_order_line_details.vendor_name": "Vendor name", "entityType.simple_service_point_detail": "Service point", "entityType.simple_service_point_detail.code": "Code", "entityType.simple_service_point_detail.jsonb": "JSONB", @@ -853,18 +551,18 @@ "entityType.simple_user_details.user_created_date": "User created date", "entityType.simple_user_details.user_updated_date": "User updated date", "entityType.simple_user_details.username": "Username", - "entityType.drv_statistical_code_details": "Statistical code", - "entityType.drv_statistical_code_details.code": "Code", - "entityType.drv_statistical_code_details.created_by_user_id": "Created by user ID", - "entityType.drv_statistical_code_details.created_by_username": "Created by username", - "entityType.drv_statistical_code_details.created_date": "Created date", - "entityType.drv_statistical_code_details.id": "ID", - "entityType.drv_statistical_code_details.name": "Name", - "entityType.drv_statistical_code_details.source": "Source", - "entityType.drv_statistical_code_details.statistical_code_type_id": "Statistical code type ID", - "entityType.drv_statistical_code_details.updated_by_user_id": "Updated by user ID", - "entityType.drv_statistical_code_details.updated_by_username": "Updated by username", - "entityType.drv_statistical_code_details.updated_date": "Updated date", + "entityType.simple_statistical_code": "Statistical code", + "entityType.simple_statistical_code.code": "Code", + "entityType.simple_statistical_code.created_by_user_id": "Created by user ID", + "entityType.simple_statistical_code.created_by_username": "Created by username", + "entityType.simple_statistical_code.created_date": "Created date", + "entityType.simple_statistical_code.id": "ID", + "entityType.simple_statistical_code.name": "Name", + "entityType.simple_statistical_code.source": "Source", + "entityType.simple_statistical_code.statistical_code_type_id": "Statistical code type ID", + "entityType.simple_statistical_code.updated_by_user_id": "Updated by user ID", + "entityType.simple_statistical_code.updated_by_username": "Updated by username", + "entityType.simple_statistical_code.updated_date": "Updated date", "entityType.drv_statistical_code_type_details": "Statistical code type", "entityType.drv_statistical_code_type_details.created_by_user_id": "Created by user ID", "entityType.drv_statistical_code_type_details.created_by_username": "Created by username", @@ -875,39 +573,6 @@ "entityType.drv_statistical_code_type_details.updated_by_user_id": "Updated by user ID", "entityType.drv_statistical_code_type_details.updated_by_username": "Updated by username", "entityType.drv_statistical_code_type_details.updated_date": "Updated date", - "entityType.drv_user_details": "Users", - "entityType.drv_user_details._custom_field_possessive": "User's {customField}", - "entityType.drv_user_details.id": "User ID", - "entityType.drv_user_details.user_active": "User active", - "entityType.drv_user_details.user_address_ids": "User address ID list", - "entityType.drv_user_details.user_address_line1": "User address list (line 1)", - "entityType.drv_user_details.user_address_line2": "User address list (line 2)", - "entityType.drv_user_details.user_address_type_names": "User address type list", - "entityType.drv_user_details.user_barcode": "User barcode", - "entityType.drv_user_details.user_cities": "User city list", - "entityType.drv_user_details.user_country_ids": "User country ID list", - "entityType.drv_user_details.user_created_date": "User created date", - "entityType.drv_user_details.user_date_of_birth": "User date of birth", - "entityType.drv_user_details.user_department_ids": "User department ID list", - "entityType.drv_user_details.user_department_names": "User department list", - "entityType.drv_user_details.user_email": "User email", - "entityType.drv_user_details.user_enrollment_date": "User enrollment date", - "entityType.drv_user_details.user_expiration_date": "User expiration date", - "entityType.drv_user_details.user_external_system_id": "User external system ID", - "entityType.drv_user_details.user_first_name": "User first name", - "entityType.drv_user_details.user_last_name": "User last name", - "entityType.drv_user_details.user_middle_name": "User middle name", - "entityType.drv_user_details.user_mobile_phone": "User mobile phone", - "entityType.drv_user_details.user_patron_group": "User patron group", - "entityType.drv_user_details.user_patron_group_id": "User patron group ID", - "entityType.drv_user_details.user_phone": "User phone", - "entityType.drv_user_details.user_postal_codes": "User postal code list", - "entityType.drv_user_details.user_preferred_contact_type": "User preferred contact type", - "entityType.drv_user_details.user_preferred_first_name": "User preferred first name", - "entityType.drv_user_details.user_primary_address": "User primary address", - "entityType.drv_user_details.user_regions": "User region list", - "entityType.drv_user_details.user_updated_date": "User updated date", - "entityType.drv_user_details.username": "Username", "entityType.drv_voucher_lines_details": "Voucher Lines", "entityType.drv_voucher_lines_details.amount": "Amount", "entityType.drv_voucher_lines_details.created_by_user_id": "Created by user ID", @@ -1824,5 +1489,7 @@ "entityType.composite_purchase_order_lines.vendor_organization": "Vendor organization", "entityType.composite_purchase_order_lines.assigned_to_user": "Assigned to user", "entityType.composite_purchase_order_lines.rates": "Exchange rate", - "entityType.composite_purchase_order_lines.pol_exchange_rate": "POL exchange rate" + "entityType.composite_purchase_order_lines.pol_exchange_rate": "POL exchange rate", + + "entityType.removed": "Removed" }