2025-07-21 16:46:58 -04:00
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
2025-07-21 17:08:45 -04:00
|
|
|
import { Pool } from 'pg';
|
2025-07-21 17:12:24 -04:00
|
|
|
|
|
|
|
|
// Use require for JSON import to avoid module issues
|
|
|
|
|
const cardsData = require('../src/data/cards.json');
|
2025-07-21 16:46:58 -04:00
|
|
|
|
2025-07-21 17:08:45 -04:00
|
|
|
// Create PostgreSQL connection pool
|
|
|
|
|
const pool = new Pool({
|
|
|
|
|
connectionString: process.env.DATABASE_URL,
|
|
|
|
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
|
|
|
|
});
|
2025-07-21 16:46:58 -04:00
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 17:08:45 -04:00
|
|
|
const client = await pool.connect();
|
|
|
|
|
|
2025-07-21 16:46:58 -04:00
|
|
|
try {
|
|
|
|
|
console.log('Starting card migration to Neon database...');
|
|
|
|
|
|
|
|
|
|
// Check if cards already exist to prevent duplicate migration
|
2025-07-21 17:08:45 -04:00
|
|
|
const existingCards = await client.query('SELECT COUNT(*) as count FROM cards');
|
|
|
|
|
const cardCount = existingCards.rows[0]?.count || 0;
|
2025-07-21 16:46:58 -04:00
|
|
|
|
|
|
|
|
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 {
|
2025-07-21 17:08:45 -04:00
|
|
|
await client.query(`
|
2025-07-21 16:46:58 -04:00
|
|
|
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 (
|
2025-07-21 17:08:45 -04:00
|
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
|
|
|
|
|
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31
|
2025-07-21 16:46:58 -04:00
|
|
|
)
|
2025-07-21 17:08:45 -04:00
|
|
|
`, [
|
|
|
|
|
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()
|
|
|
|
|
]);
|
2025-07-21 16:46:58 -04:00
|
|
|
migratedCount++;
|
|
|
|
|
} catch (error) {
|
2025-07-21 16:56:26 -04:00
|
|
|
const errorMsg = `Error migrating card ${card.name}: ${(error as Error).message}`;
|
2025-07-21 16:46:58 -04:00
|
|
|
console.error(errorMsg);
|
|
|
|
|
errors.push(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get final count to verify
|
2025-07-21 17:08:45 -04:00
|
|
|
const finalCount = await client.query('SELECT COUNT(*) as count FROM cards');
|
|
|
|
|
const finalCardCount = finalCount.rows[0]?.count || 0;
|
2025-07-21 16:46:58 -04:00
|
|
|
|
|
|
|
|
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',
|
2025-07-21 16:56:26 -04:00
|
|
|
details: (error as Error).message
|
2025-07-21 16:46:58 -04:00
|
|
|
}), {
|
|
|
|
|
status: 500,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
});
|
2025-07-21 17:08:45 -04:00
|
|
|
} finally {
|
|
|
|
|
client.release();
|
2025-07-21 16:46:58 -04:00
|
|
|
}
|
|
|
|
|
}
|