/** * Migration script to upload card hashes from SQLite to Convex. * * Usage: * npx tsx scripts/migrate-hashes.ts * * Prerequisites: * - Run `npx convex dev` first to set up the Convex project * - Ensure CONVEX_URL is set in .env.local */ import { ConvexHttpClient } from "convex/browser"; import Database from "better-sqlite3"; import { api } from "../convex/_generated/api"; import * as dotenv from "dotenv"; import * as path from "path"; import * as fs from "fs"; // Load environment variables dotenv.config({ path: path.join(__dirname, "..", ".env.local") }); const CONVEX_URL = process.env.EXPO_PUBLIC_CONVEX_URL || process.env.CONVEX_URL; const DB_PATH = process.env.DB_PATH || path.join(__dirname, "..", "card_hashes.db"); const BATCH_SIZE = 50; const HASH_VERSION = 1; interface CardRow { id: string; oracle_id: string; name: string; set_code: string; collector_number: string | null; rarity: string | null; artist: string | null; image_uri: string | null; hash: Buffer; } async function main() { if (!CONVEX_URL) { console.error("Error: CONVEX_URL not set in .env.local"); console.error("Run 'npx convex dev --once --configure=new' first"); process.exit(1); } if (!fs.existsSync(DB_PATH)) { console.error(`Error: Database not found at ${DB_PATH}`); process.exit(1); } console.log(`Connecting to Convex: ${CONVEX_URL}`); console.log(`Reading from SQLite: ${DB_PATH}`); const client = new ConvexHttpClient(CONVEX_URL); const db = new Database(DB_PATH, { readonly: true }); try { // Get total count const countRow = db .prepare("SELECT COUNT(*) as count FROM cards WHERE hash IS NOT NULL") .get() as { count: number }; const totalCards = countRow.count; console.log(`Found ${totalCards} cards with hashes to migrate`); // Query cards with hashes const stmt = db.prepare(` SELECT id, oracle_id, name, set_code, collector_number, rarity, artist, image_uri, hash FROM cards WHERE hash IS NOT NULL ORDER BY name `); const cards = stmt.all() as CardRow[]; let migrated = 0; let errors = 0; // Process in batches for (let i = 0; i < cards.length; i += BATCH_SIZE) { const batch = cards.slice(i, i + BATCH_SIZE); const convexCards = batch.map((card) => ({ scryfallId: card.id, oracleId: card.oracle_id, name: card.name, setCode: card.set_code, collectorNumber: card.collector_number || "", rarity: card.rarity || "common", artist: card.artist || undefined, imageUri: card.image_uri || undefined, hash: new Uint8Array(card.hash).buffer as ArrayBuffer, hashVersion: HASH_VERSION, })); try { await client.mutation(api.cards.insertBatch, { cards: convexCards }); migrated += batch.length; const pct = ((migrated / totalCards) * 100).toFixed(1); process.stdout.write(`\rMigrated ${migrated}/${totalCards} (${pct}%)`); } catch (err) { console.error(`\nError migrating batch starting at ${i}:`, err); errors += batch.length; } } console.log(`\n\nMigration complete!`); console.log(` Migrated: ${migrated}`); console.log(` Errors: ${errors}`); } finally { db.close(); } } main().catch((err) => { console.error("Migration failed:", err); process.exit(1); });