feat(migration): add Supabase import and recovery tooling

This commit is contained in:
poyrazavsever
2026-07-17 09:13:06 +03:00
parent 64a5f96872
commit 5948907e5d
10 changed files with 5525 additions and 9 deletions
+86
View File
@@ -0,0 +1,86 @@
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { applySqlitePragmas, ensureDataLayout, getDataConfig } from "./lib/data-dir.mjs";
import {
applySupabaseImport,
assertTargetImportSafety,
loadImportBundle,
prepareSupabaseImport,
rollbackStagedFiles,
stageImportFiles,
validateTargetOwner,
} from "./lib/supabase-import.mjs";
const args = parseArgs(process.argv.slice(2));
if (!args.from || !args.ownerUserId) {
throw new Error(
"Usage: node scripts/import-supabase.mjs --from <export-dir> --owner-user-id <better-auth-user-id> [--dry-run] [--allow-existing] [--report <path>]",
);
}
const config = ensureDataLayout(getDataConfig(process.env));
const { bundle, bundleDir, bundlePath } = loadImportBundle(args.from);
const plan = prepareSupabaseImport({
bundle,
bundleDir,
targetOwnerUserId: args.ownerUserId,
});
const db = new Database(config.databasePath);
let stagedFiles = [];
try {
applySqlitePragmas(db);
const owner = validateTargetOwner(db, args.ownerUserId);
const existingCounts = assertTargetImportSafety(db, plan, args.allowExisting || args.dryRun);
const report = {
format: "neta-supabase-import-report",
version: 1,
mode: args.dryRun ? "dry-run" : "apply",
exportPath: bundlePath,
sourceOwnerUserId: plan.sourceOwnerUserId,
targetOwnerUserId: plan.targetOwnerUserId,
targetOwnerEmail: owner.email,
sourceCounts: plan.sourceCounts,
targetCounts: plan.targetCounts,
existingCounts,
warnings: plan.warnings,
verification: null,
completedAt: null,
};
if (!args.dryRun) {
stagedFiles = stageImportFiles(plan, config.uploadsDir);
try {
report.verification = applySupabaseImport(db, plan);
report.completedAt = new Date().toISOString();
} catch (error) {
rollbackStagedFiles(stagedFiles);
throw error;
}
}
const reportPath = path.resolve(
args.report ?? path.join(config.dataDir, args.dryRun ? "import-dry-run-report.json" : "import-report.json"),
);
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
console.log(`${args.dryRun ? "Import dry-run" : "Import"} completed. Report: ${reportPath}`);
for (const warning of report.warnings) console.warn(`Warning: ${warning}`);
} finally {
db.close();
}
function parseArgs(values) {
const parsed = { dryRun: false, allowExisting: false };
for (let index = 0; index < values.length; index += 1) {
const value = values[index];
if (value === "--dry-run") parsed.dryRun = true;
else if (value === "--allow-existing") parsed.allowExisting = true;
else if (value === "--from") parsed.from = values[++index];
else if (value === "--owner-user-id") parsed.ownerUserId = values[++index];
else if (value === "--report") parsed.report = values[++index];
else throw new Error(`Unknown argument: ${value}`);
}
return parsed;
}