- 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
131 lines
3.6 KiB
TypeScript
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);
|
|
});
|