102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
|
import type { CreateMigrationReportInput } from "../../shared/commands";
|
|
import { createId } from "../../shared/ids";
|
|
import { AuditService } from "./audit-service";
|
|
|
|
const LEGACY_TABLES = [
|
|
"organizations",
|
|
"agents",
|
|
"tasks",
|
|
"approvals",
|
|
"chat_messages",
|
|
"providers",
|
|
"skill_definitions",
|
|
"file_objects",
|
|
"social_accounts",
|
|
];
|
|
|
|
export class MigrationService {
|
|
private readonly audit: AuditService;
|
|
|
|
constructor(private readonly db: DatabaseSync) {
|
|
this.audit = new AuditService(db);
|
|
}
|
|
|
|
createReport(input: CreateMigrationReportInput): string {
|
|
if (!existsSync(input.sourceDatabasePath)) throw new Error("Source database was not found.");
|
|
const source = new DatabaseSync(input.sourceDatabasePath, { readOnly: true });
|
|
try {
|
|
const availableTables = new Set(
|
|
source.prepare("select name from sqlite_master where type = 'table'")
|
|
.all()
|
|
.map((row) => (row as { name: string }).name),
|
|
);
|
|
const tableCounts = Object.fromEntries(
|
|
LEGACY_TABLES
|
|
.filter((table) => availableTables.has(table))
|
|
.map((table) => [table, countRows(source.prepare(`select count(*) as count from ${table}`))]),
|
|
);
|
|
const tableChecksums = Object.fromEntries(
|
|
LEGACY_TABLES
|
|
.filter((table) => availableTables.has(table))
|
|
.map((table) => [table, tableChecksum(source, table)]),
|
|
);
|
|
const idMaps = Object.fromEntries(
|
|
LEGACY_TABLES
|
|
.filter((table) => availableTables.has(table))
|
|
.map((table) => [table, tableIds(source, table)]),
|
|
);
|
|
const missingTables = LEGACY_TABLES.filter((table) => !availableTables.has(table));
|
|
const failures = missingTables.map((table) => ({ table, reason: "missing_table" }));
|
|
const reportId = createId("mig");
|
|
const now = new Date().toISOString();
|
|
this.db.prepare(`
|
|
insert into migration_reports(id, org_id, source_ref, report_json, created_at)
|
|
values (?, ?, ?, ?, ?)
|
|
`).run(reportId, input.orgId, input.sourceDatabasePath, JSON.stringify({
|
|
tableCounts,
|
|
tableChecksums,
|
|
idMaps,
|
|
missingTables,
|
|
failures,
|
|
rollbackPack: {
|
|
sourceDatabasePath: input.sourceDatabasePath,
|
|
capturedAt: now,
|
|
restoreStrategy: "legacy_snapshot_restore",
|
|
},
|
|
}), now);
|
|
this.audit.record({
|
|
orgId: input.orgId,
|
|
entityType: "migration_report",
|
|
entityId: reportId,
|
|
actorType: "system",
|
|
actorId: null,
|
|
action: "created",
|
|
payload: { sourceRef: input.sourceDatabasePath, tableCount: Object.keys(tableCounts).length },
|
|
});
|
|
return reportId;
|
|
} finally {
|
|
source.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
function countRows(statement: StatementSync): number {
|
|
return (statement.get() as { count: number }).count;
|
|
}
|
|
|
|
function tableChecksum(source: DatabaseSync, table: string): string {
|
|
const hash = createHash("sha256");
|
|
const rows = source.prepare(`select * from ${table} order by rowid`).all();
|
|
for (const row of rows) hash.update(JSON.stringify(row));
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
function tableIds(source: DatabaseSync, table: string): string[] {
|
|
const columns = source.prepare(`pragma table_info(${table})`).all() as Array<{ name: string }>;
|
|
if (!columns.some((column) => column.name === "id")) return [];
|
|
return source.prepare(`select id from ${table} order by id`).all().map((row) => String((row as { id: unknown }).id));
|
|
}
|