Handle missing tables in tenant merge
All checks were successful
Build and Push Docker Images / build-central-services-api (push) Successful in 24s
Build and Push Docker Images / build-central-services-admin (push) Successful in 24s
Build and Push Docker Images / build-docs (push) Successful in 24s
Build and Push Docker Images / build-backend (push) Successful in 38s
Build and Push Docker Images / build-frontend (push) Successful in 1m42s
Build and Push Docker Images / build-website (push) Successful in 27s

This commit is contained in:
2026-08-06 14:52:38 +02:00
parent 5a05d2e9e8
commit c66d04f0b5
2 changed files with 46 additions and 4 deletions

View File

@@ -80,6 +80,42 @@ const remapSourceTenant = (exportData: TenantFullExport, targetTenantId: number)
return tables
}
const splitSourceTablesByAvailability = (
sourceTables: Record<string, Record<string, any>[]>,
metadata: Record<string, MergeDatabaseMetadata>
) => {
const available: Record<string, Record<string, any>[]> = {}
const unavailable = Object.keys(sourceTables).filter((table) => {
if (metadata[table]) {
available[table] = sourceTables[table]
return false
}
return sourceTables[table].length > 0
})
return { available, unavailable }
}
const appendUnavailableTableConflicts = (plan: TenantMergePlan, unavailableTables: string[]) => {
for (const table of unavailableTables.sort()) {
plan.items.push({
id: `missing-target-table:${table}`,
table,
kind: "conflict",
sourceKey: "Zieltabelle fehlt",
targetKey: null,
label: `Tabelle ${table} ist im Zielsystem nicht vorhanden`,
defaultDecision: "target",
allowedDecisions: ["target"],
differences: ["Die erforderliche Tabelle fehlt im Zielschema und muss zuerst per Migration angelegt werden."],
sourceRow: {},
targetRow: null,
})
plan.summary.conflict += 1
}
return plan
}
const loadTargetRows = async (
client: any,
table: string,
@@ -124,7 +160,8 @@ export const createTenantMergeDryRunWithClient = async (
targetTenantId: number
): Promise<TenantMergePlan> => {
const metadata = await loadMergeMetadata(client)
const sourceTables = remapSourceTenant(exportData, targetTenantId)
const remappedSourceTables = remapSourceTenant(exportData, targetTenantId)
const { available: sourceTables, unavailable } = splitSourceTablesByAvailability(remappedSourceTables, metadata)
const targetTables: Record<string, Record<string, any>[]> = {}
for (const [table, sourceRows] of Object.entries(sourceTables)) {
@@ -133,7 +170,7 @@ export const createTenantMergeDryRunWithClient = async (
targetTables[table] = await loadTargetRows(client, table, sourceRows, tableMetadata, targetTenantId)
}
return buildTenantMergePlan(sourceTables, targetTables, metadata)
return appendUnavailableTableConflicts(buildTenantMergePlan(sourceTables, targetTables, metadata), unavailable)
}
export const createTenantMergeDryRun = async (
@@ -214,7 +251,9 @@ const topologicalTableOrder = (tables: string[], metadata: Record<string, MergeD
const ordered: string[] = []
while (remaining.size) {
const ready = Array.from(remaining).filter((table) =>
metadata[table].foreignKeys.every((foreignKey) => !remaining.has(foreignKey.referencedTable) || foreignKey.referencedTable === table)
(metadata[table]?.foreignKeys || []).every((foreignKey) =>
!remaining.has(foreignKey.referencedTable) || foreignKey.referencedTable === table
)
)
const next = ready.length ? ready.sort() : [Array.from(remaining).sort()[0]]
for (const table of next) {
@@ -240,7 +279,8 @@ export const executeTenantMergeWithClient = async (
decisions: Record<string, TenantMergeDecision>
): Promise<TenantMergeExecutionResult> => {
const metadata = await loadMergeMetadata(client)
const sourceTables = remapSourceTenant(rawExportData, targetTenantId)
const remappedSourceTables = remapSourceTenant(rawExportData, targetTenantId)
const { available: sourceTables } = splitSourceTablesByAvailability(remappedSourceTables, metadata)
const preparedExport: TenantFullExport = prepareTenantFullExportRowsForImport({
...rawExportData,
tenantId: targetTenantId,

View File

@@ -217,12 +217,14 @@ onMounted(async () => {
</p>
<div v-if="item.kind === 'conflict'" class="flex flex-wrap gap-2">
<UButton
v-if="item.allowedDecisions.includes('target')"
size="sm"
:color="decisions[item.id] === 'target' ? 'primary' : 'neutral'"
:variant="decisions[item.id] === 'target' ? 'solid' : 'soft'"
@click="decisions[item.id] = 'target'"
>Ziel behalten</UButton>
<UButton
v-if="item.allowedDecisions.includes('source')"
size="sm"
:color="decisions[item.id] === 'source' ? 'warning' : 'neutral'"
:variant="decisions[item.id] === 'source' ? 'solid' : 'soft'"