echos-ocr/scripts/verify-migration.ts
Randall Stillwell 21c863da01 Migrate to Vercel + Supabase + Upstash production stack
- Replace pdf2pic/GraphicsMagick with pdfjs-dist + @napi-rs/canvas for
  Vercel-compatible PDF rasterization
- Replace MinIO with Supabase Storage (S3-compatible); rename minio.ts
  to storage.ts and update all imports
- Replace in-memory job queue with Upstash QStash; upload route now
  persists files to storage before enqueuing, /api/jobs/process handles
  the QStash callback
- Convert email watcher from persistent IMAP connection to stateless
  scanInbox() polled by Vercel Cron every 2 minutes
- Add FTP watcher (basic-ftp) with cron polling for scanner integration
  via Dreamhost FTP drop directory
- Add FTP config fields to AppSettings schema
- Remove folder watcher (chokidar), standalone output, Docker-only code
- Update next.config.ts, middleware, instrumentation for serverless
- Add vercel.json with cron schedules for email and FTP polling
- Add migration scripts for database (pg_dump/restore) and storage
  (S3-to-S3 copy) with verification

Made-with: Cursor
2026-04-15 23:51:44 -05:00

131 lines
3.6 KiB
TypeScript

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