deckhearth/api/migrate.ts
Randall Stillwell 4821d1a389 Switch from JSON to Neon database for card data
- Add /api/migrate endpoint to populate Neon database with existing JSON data
- Update /api/v1/cards/ to query Neon database instead of JSON file
- Update /api/v1/cards/[id] to fetch single cards from Neon
- Add proper error handling and development debugging
- Maintain backward compatibility with existing API interface
- Ready to migrate card data to PostgreSQL
2025-07-21 15:46:58 -05:00

155 lines
No EOL
5.1 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { neon } from '@neondatabase/serverless';
import cardsData from '../src/data/cards.json';
// Initialize Neon client
const sql = neon(process.env.DATABASE_URL!);
interface JsonCard {
id: number;
name: string;
set_name?: string;
set_code?: string;
card_number?: string;
rarity?: string;
game: string;
mana_cost?: string;
cmc?: number;
card_type?: string;
colors?: string[] | null;
oracle_text?: string;
flavor_text?: string | null;
power?: string | null;
toughness?: string | null;
loyalty?: string | null;
artist?: string | null;
image_url?: string;
stock_image_url?: string;
artwork_crop_coords?: any;
current_price?: number;
market_price?: number;
low_price?: number | null;
high_price?: number | null;
price_last_updated?: string | null;
ocr_confidence?: number | null;
ocr_raw_text?: string | null;
scryfall_id?: string | null;
tcg_player_id?: string | null;
verified: number; // JSON has number, but we want boolean
created_at?: string;
updated_at?: string;
}
export default async function handler(req: NextRequest) {
// Only allow POST requests for security
if (req.method !== 'POST') {
return new NextResponse(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: { 'Content-Type': 'application/json' },
});
}
try {
console.log('Starting card migration to Neon database...');
// Check if cards already exist to prevent duplicate migration
const existingCards = await sql`SELECT COUNT(*) as count FROM cards`;
const cardCount = existingCards[0]?.count || 0;
if (cardCount > 0) {
return new NextResponse(JSON.stringify({
message: `Database already contains ${cardCount} cards. Migration skipped.`,
cards_count: cardCount
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// Migrate cards from JSON
const cards = cardsData as JsonCard[];
console.log(`Migrating ${cards.length} cards...`);
let migratedCount = 0;
const errors: string[] = [];
for (const card of cards) {
try {
await sql`
INSERT INTO cards (
name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
card_type, colors, oracle_text, flavor_text, power, toughness, loyalty,
artist, image_url, stock_image_url, artwork_crop_coords, current_price,
market_price, low_price, high_price, price_last_updated, ocr_confidence,
ocr_raw_text, scryfall_id, tcg_player_id, verified, created_at, updated_at
) VALUES (
${card.name},
${card.set_name || null},
${card.set_code || null},
${card.card_number || null},
${card.rarity || null},
${card.game},
${card.mana_cost || null},
${card.cmc || null},
${card.card_type || null},
${card.colors ? JSON.stringify(card.colors) : null},
${card.oracle_text || null},
${card.flavor_text || null},
${card.power || null},
${card.toughness || null},
${card.loyalty || null},
${card.artist || null},
${card.image_url || null},
${card.stock_image_url || null},
${card.artwork_crop_coords ? JSON.stringify(card.artwork_crop_coords) : null},
${card.current_price || null},
${card.market_price || null},
${card.low_price || null},
${card.high_price || null},
${card.price_last_updated ? new Date(card.price_last_updated) : null},
${card.ocr_confidence || null},
${card.ocr_raw_text || null},
${card.scryfall_id || null},
${card.tcg_player_id || null},
${Boolean(card.verified)},
${card.created_at ? new Date(card.created_at) : new Date()},
${card.updated_at ? new Date(card.updated_at) : new Date()}
)
`;
migratedCount++;
} catch (error) {
const errorMsg = `Error migrating card ${card.name}: ${error.message}`;
console.error(errorMsg);
errors.push(errorMsg);
}
}
// Get final count to verify
const finalCount = await sql`SELECT COUNT(*) as count FROM cards`;
const finalCardCount = finalCount[0]?.count || 0;
console.log(`✅ Successfully migrated ${migratedCount} cards to Neon database!`);
return new NextResponse(JSON.stringify({
success: true,
message: `Successfully migrated ${migratedCount} out of ${cards.length} cards`,
cards_migrated: migratedCount,
total_cards_in_db: finalCardCount,
errors: errors.length > 0 ? errors : undefined
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Migration failed:', error);
return new NextResponse(JSON.stringify({
success: false,
error: 'Migration failed',
details: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}