Migrate from .NET MAUI to Expo + Convex
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>
This commit is contained in:
parent
56499d5af9
commit
83ab4df537
138 changed files with 19136 additions and 7681 deletions
116
scripts/migrate-hashes.ts
Normal file
116
scripts/migrate-hashes.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue