echos-ocr/scripts/verify-migration.ts

132 lines
3.6 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<number> {
const res = await pool.query(`SELECT count(*)::int as cnt FROM "${table}"`);
return res.rows[0].cnt;
}
async function countStorageObjects(prefix: string): Promise<number> {
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);
});