Connect tenant merge planner to exports
This commit is contained in:
@@ -1025,41 +1025,14 @@ export const importTenantFullExportArchive = async (
|
||||
archiveBuffer: Buffer,
|
||||
options: ImportOptions = {}
|
||||
): Promise<ImportResult> => {
|
||||
const rawExportData = await readTenantFullExportArchive(archiveBuffer)
|
||||
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
||||
const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))
|
||||
|
||||
try {
|
||||
const entries = await reader.getEntries()
|
||||
const entriesByName = new Map(entries.map((entry: any) => [entry.filename, entry]))
|
||||
const manifest = JSON.parse(await readZipTextEntry(entriesByName, "manifest.json")) as TenantArchiveManifest
|
||||
|
||||
if (manifest?.format !== "fedeo.tenant-archive-export" || manifest.version !== 1) {
|
||||
throw new Error("Ungültiges FEDEO Mandantenarchiv-Format")
|
||||
}
|
||||
|
||||
const tables: TableRows = {}
|
||||
for (const table of manifest.tables || []) {
|
||||
tables[table.name] = JSON.parse(await readZipTextEntry(entriesByName, table.path))
|
||||
}
|
||||
|
||||
const rawExportData: TenantFullExport = {
|
||||
format: "fedeo.tenant-full-export",
|
||||
version: 1,
|
||||
exportedAt: manifest.exportedAt,
|
||||
tenantId: manifest.tenantId,
|
||||
tables,
|
||||
files: (manifest.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
contentBase64: null,
|
||||
missing: file.missing,
|
||||
error: file.error,
|
||||
})),
|
||||
}
|
||||
|
||||
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
||||
const sourceTenantId = rawExportData.tenantId
|
||||
const targetTenantId = exportData.tenantId
|
||||
const sourcePrefix = `${sourceTenantId}/`
|
||||
@@ -1086,3 +1059,43 @@ export const importTenantFullExportArchive = async (
|
||||
await reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const readTenantFullExportArchive = async (archiveBuffer: Buffer): Promise<TenantFullExport> => {
|
||||
const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))
|
||||
|
||||
try {
|
||||
const entries = await reader.getEntries()
|
||||
const entriesByName = new Map(entries.map((entry: any) => [entry.filename, entry]))
|
||||
const manifest = JSON.parse(await readZipTextEntry(entriesByName, "manifest.json")) as TenantArchiveManifest
|
||||
|
||||
if (manifest?.format !== "fedeo.tenant-archive-export" || manifest.version !== 1) {
|
||||
throw new Error("Ungültiges FEDEO Mandantenarchiv-Format")
|
||||
}
|
||||
|
||||
const tables: TableRows = {}
|
||||
for (const table of manifest.tables || []) {
|
||||
tables[table.name] = JSON.parse(await readZipTextEntry(entriesByName, table.path))
|
||||
}
|
||||
|
||||
return {
|
||||
format: "fedeo.tenant-full-export",
|
||||
version: 1,
|
||||
exportedAt: manifest.exportedAt,
|
||||
tenantId: manifest.tenantId,
|
||||
tables,
|
||||
files: (manifest.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
contentBase64: null,
|
||||
missing: file.missing,
|
||||
error: file.error,
|
||||
})),
|
||||
}
|
||||
|
||||
} finally {
|
||||
await reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
124
backend/src/utils/tenantMergeService.ts
Normal file
124
backend/src/utils/tenantMergeService.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user