Files
AgentCompany/AgentCompany-New/src/main/services/migration-service.ts
T

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));
}