Connect tenant merge planner to exports

This commit is contained in:
2026-08-06 12:26:00 +02:00
parent f691e3db33
commit fd330b28c2
2 changed files with 166 additions and 29 deletions

View File

@@ -0,0 +1,124 @@
import { pool } from "../../db"
import type { TenantFullExport } from "./tenantFullExport"
import { buildTenantMergePlan, type TenantMergePlan, type TenantMergeTableMetadata } from "./tenantMergePlan"
type MergeDatabaseMetadata = TenantMergeTableMetadata & {
columns: string[]
}
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
const globalNaturalKeyTables = new Set(["accounts", "units", "countrys", "citys"])
const loadMergeMetadata = async (client: any) => {
const columnsResult = await client.query(`
select table_name, column_name
from information_schema.columns
where table_schema = 'public'
order by table_name, ordinal_position
`)
const primaryKeyResult = await client.query(`
select tc.table_name, kcu.column_name
from information_schema.table_constraints tc
join information_schema.key_column_usage kcu
on tc.constraint_name = kcu.constraint_name
and tc.constraint_schema = kcu.constraint_schema
where tc.table_schema = 'public' and tc.constraint_type = 'PRIMARY KEY'
order by tc.table_name, kcu.ordinal_position
`)
const metadata: Record<string, MergeDatabaseMetadata> = {}
for (const row of columnsResult.rows) {
metadata[row.table_name] ||= { columns: [], primaryKey: [] }
metadata[row.table_name].columns.push(row.column_name)
}
for (const row of primaryKeyResult.rows) {
metadata[row.table_name] ||= { columns: [], primaryKey: [] }
metadata[row.table_name].primaryKey.push(row.column_name)
}
return metadata
}
const remapSourceTenant = (exportData: TenantFullExport, targetTenantId: number) => {
const tables: Record<string, Record<string, any>[]> = {}
for (const [table, rows] of Object.entries(exportData.tables || {})) {
tables[table] = rows.map((row) => {
const next = { ...row }
if (table === "tenants" && Number(next.id) === Number(exportData.tenantId)) next.id = targetTenantId
if (Number(next.tenant) === Number(exportData.tenantId)) next.tenant = targetTenantId
if (Number(next.tenant_id) === Number(exportData.tenantId)) next.tenant_id = targetTenantId
return next
})
}
return tables
}
const loadTargetRows = async (
client: any,
table: string,
sourceRows: Record<string, any>[],
metadata: MergeDatabaseMetadata,
targetTenantId: number
) => {
if (!sourceRows.length) return []
if (table === "tenants") {
return (await client.query(`select * from ${quoteIdent(table)} where ${quoteIdent("id")} = $1`, [targetTenantId])).rows
}
const tenantColumn = metadata.columns.includes("tenant")
? "tenant"
: metadata.columns.includes("tenant_id") ? "tenant_id" : null
if (tenantColumn) {
return (await client.query(
`select * from ${quoteIdent(table)} where ${quoteIdent(tenantColumn)} = $1`,
[targetTenantId]
)).rows
}
if (globalNaturalKeyTables.has(table)) {
return (await client.query(`select * from ${quoteIdent(table)}`)).rows
}
if (metadata.primaryKey.length === 1) {
const key = metadata.primaryKey[0]
const values = Array.from(new Set(sourceRows.map((row) => row[key]).filter((value) => value !== null && typeof value !== "undefined").map(String)))
if (!values.length) return []
return (await client.query(
`select * from ${quoteIdent(table)} where ${quoteIdent(key)}::text = any($1::text[])`,
[values]
)).rows
}
return (await client.query(`select * from ${quoteIdent(table)}`)).rows
}
export const createTenantMergeDryRunWithClient = async (
client: any,
exportData: TenantFullExport,
targetTenantId: number
): Promise<TenantMergePlan> => {
const metadata = await loadMergeMetadata(client)
const sourceTables = remapSourceTenant(exportData, targetTenantId)
const targetTables: Record<string, Record<string, any>[]> = {}
for (const [table, sourceRows] of Object.entries(sourceTables)) {
const tableMetadata = metadata[table]
if (!tableMetadata) continue
targetTables[table] = await loadTargetRows(client, table, sourceRows, tableMetadata, targetTenantId)
}
return buildTenantMergePlan(sourceTables, targetTables, metadata)
}
export const createTenantMergeDryRun = async (
exportData: TenantFullExport,
targetTenantId: number
) => {
const client = await pool.connect()
try {
return await createTenantMergeDryRunWithClient(client, exportData, targetTenantId)
} finally {
client.release()
}
}