/** * Post-Migration Verification * * Checks that the database and storage are intact after migration. * * Usage: * DATABASE_URL="postgresql://..." \ * STORAGE_ENDPOINT="https://..." \ * STORAGE_ACCESS_KEY="..." \ * STORAGE_SECRET_KEY="..." \ * STORAGE_BUCKET="echos-ocr" \ * npx tsx scripts/verify-migration.ts */ import "dotenv/config"; import pg from "pg"; import { S3Client, ListObjectsV2Command, HeadObjectCommand } from "@aws-sdk/client-s3"; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 3, }); const s3 = new S3Client({ endpoint: process.env.STORAGE_ENDPOINT || "", region: process.env.STORAGE_REGION || "us-east-1", credentials: { accessKeyId: process.env.STORAGE_ACCESS_KEY || "", secretAccessKey: process.env.STORAGE_SECRET_KEY || "", }, forcePathStyle: true, }); const BUCKET = process.env.STORAGE_BUCKET || "echos-ocr"; async function countTable(table: string): Promise { const res = await pool.query(`SELECT count(*)::int as cnt FROM "${table}"`); return res.rows[0].cnt; } async function countStorageObjects(prefix: string): Promise { let count = 0; let continuationToken: string | undefined; do { const res = await s3.send( new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix, ContinuationToken: continuationToken, }) ); count += res.KeyCount || 0; continuationToken = res.NextContinuationToken; } while (continuationToken); return count; } async function spotCheck(): Promise<{ checked: number; ok: number; missing: number }> { const res = await pool.query(` SELECT id, "frontImagePath", "backImagePath" FROM "ResponseCard" WHERE "frontImagePath" IS NOT NULL OR "backImagePath" IS NOT NULL ORDER BY "createdAt" DESC LIMIT 10 `); let checked = 0; let ok = 0; let missing = 0; for (const row of res.rows) { for (const path of [row.frontImagePath, row.backImagePath]) { if (!path) continue; checked++; try { await s3.send(new HeadObjectCommand({ Bucket: BUCKET, Key: path })); ok++; } catch { console.log(` MISSING: ${path} (card ${row.id})`); missing++; } } } return { checked, ok, missing }; } async function main() { console.log("=== Post-Migration Verification ===\n"); console.log("--- Database Row Counts ---"); const tables = [ "User", "Organization", "OrgMember", "Location", "ResponseCard", "ProcessingJob", "ActivityLog", "Notification", "Integration", "AppSettings", ]; for (const table of tables) { try { const count = await countTable(table); console.log(` ${table.padEnd(20)} ${count}`); } catch (err) { console.log(` ${table.padEnd(20)} ERROR: ${(err as Error).message}`); } } console.log("\n--- Storage Object Counts ---"); const imageCount = await countStorageObjects("images/"); const sourceCount = await countStorageObjects("sources/"); console.log(` images/ ${imageCount}`); console.log(` sources/ ${sourceCount}`); console.log("\n--- Spot Check: 10 newest cards ---"); const spotResult = await spotCheck(); console.log(` Checked: ${spotResult.checked}, OK: ${spotResult.ok}, Missing: ${spotResult.missing}`); if (spotResult.missing > 0) { console.log("\n WARNING: Some storage objects are missing. Storage migration may be incomplete."); } else { console.log("\n All spot-checked objects found in storage."); } console.log("\n=== Verification Complete ==="); await pool.end(); } main().catch((err) => { console.error("Fatal error:", err); process.exit(1); });