Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|