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
This commit is contained in:
parent
4fea3d287e
commit
4821d1a389
3 changed files with 232 additions and 25 deletions
155
api/migrate.ts
Normal file
155
api/migrate.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import cardsData from '../../../src/data/cards.json'
|
||||
import { neon } from '@neondatabase/serverless'
|
||||
|
||||
// Initialize Neon client
|
||||
const sql = neon(process.env.DATABASE_URL!)
|
||||
|
||||
interface Card {
|
||||
id: number
|
||||
|
|
@ -40,12 +43,23 @@ export default async function handler(req: NextRequest) {
|
|||
return new NextResponse(null, { status: 200, headers })
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const pathSegments = url.pathname.split('/')
|
||||
const cardId = parseInt(pathSegments[pathSegments.length - 1])
|
||||
if (req.method !== 'GET') {
|
||||
return new NextResponse(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (isNaN(cardId)) {
|
||||
try {
|
||||
// Extract card ID from URL
|
||||
const url = new URL(req.url)
|
||||
const pathParts = url.pathname.split('/')
|
||||
const cardId = pathParts[pathParts.length - 1]
|
||||
|
||||
if (!cardId || isNaN(parseInt(cardId))) {
|
||||
return new NextResponse(JSON.stringify({ error: 'Invalid card ID' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
|
|
@ -55,10 +69,10 @@ export default async function handler(req: NextRequest) {
|
|||
})
|
||||
}
|
||||
|
||||
const cards: Card[] = cardsData as Card[]
|
||||
const card = cards.find(c => c.id === cardId)
|
||||
// Query the database for the specific card
|
||||
const result = await sql`SELECT * FROM cards WHERE id = ${parseInt(cardId)}`
|
||||
|
||||
if (!card) {
|
||||
if (result.length === 0) {
|
||||
return new NextResponse(JSON.stringify({ error: 'Card not found' }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
|
|
@ -68,6 +82,14 @@ export default async function handler(req: NextRequest) {
|
|||
})
|
||||
}
|
||||
|
||||
// Transform the result to match expected format
|
||||
const card = {
|
||||
...result[0],
|
||||
colors: result[0].colors ? (typeof result[0].colors === 'string' ? JSON.parse(result[0].colors) : result[0].colors) : null,
|
||||
artwork_crop_coords: result[0].artwork_crop_coords ?
|
||||
(typeof result[0].artwork_crop_coords === 'string' ? JSON.parse(result[0].artwork_crop_coords) : result[0].artwork_crop_coords) : null
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify(card), {
|
||||
status: 200,
|
||||
headers: {
|
||||
|
|
@ -78,7 +100,10 @@ export default async function handler(req: NextRequest) {
|
|||
|
||||
} catch (error) {
|
||||
console.error('API Error:', error)
|
||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
||||
return new NextResponse(JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
...headers,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import cardsData from '../../../src/data/cards.json'
|
||||
import { neon } from '@neondatabase/serverless'
|
||||
|
||||
// Initialize Neon client
|
||||
const sql = neon(process.env.DATABASE_URL!)
|
||||
|
||||
interface Card {
|
||||
id: number
|
||||
|
|
@ -51,30 +54,51 @@ export default async function handler(req: NextRequest) {
|
|||
const game = searchParams.get('game')
|
||||
const set_name = searchParams.get('set_name')
|
||||
|
||||
let cards: Card[] = cardsData as Card[]
|
||||
// Build the query dynamically using template literals
|
||||
let baseQuery = 'SELECT * FROM cards WHERE 1=1'
|
||||
let queryConditions: string[] = []
|
||||
let queryValues: any[] = []
|
||||
|
||||
// Apply filters
|
||||
if (game) {
|
||||
cards = cards.filter(card => card.game === game)
|
||||
queryConditions.push(`game = '${game.replace(/'/g, "''")}'`)
|
||||
}
|
||||
|
||||
if (set_name) {
|
||||
cards = cards.filter(card => card.set_name === set_name)
|
||||
queryConditions.push(`set_name = '${set_name.replace(/'/g, "''")}'`)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase()
|
||||
cards = cards.filter(card =>
|
||||
card.name.toLowerCase().includes(searchLower) ||
|
||||
(card.oracle_text && card.oracle_text.toLowerCase().includes(searchLower)) ||
|
||||
(card.card_type && card.card_type.toLowerCase().includes(searchLower))
|
||||
)
|
||||
const escapedSearch = search.replace(/'/g, "''")
|
||||
queryConditions.push(`(name ILIKE '%${escapedSearch}%' OR oracle_text ILIKE '%${escapedSearch}%' OR card_type ILIKE '%${escapedSearch}%')`)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
const paginatedCards = cards.slice(skip, skip + limit)
|
||||
// Combine conditions
|
||||
if (queryConditions.length > 0) {
|
||||
baseQuery += ' AND ' + queryConditions.join(' AND ')
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify(paginatedCards), {
|
||||
baseQuery += ' ORDER BY name'
|
||||
|
||||
if (limit > 0) {
|
||||
baseQuery += ` LIMIT ${limit}`
|
||||
}
|
||||
|
||||
if (skip > 0) {
|
||||
baseQuery += ` OFFSET ${skip}`
|
||||
}
|
||||
|
||||
// Execute the query using unsafe (for dynamic queries)
|
||||
const result = await sql.unsafe(baseQuery) as unknown as any[]
|
||||
|
||||
// Transform the result to match expected format
|
||||
const cards = result.map((row: any) => ({
|
||||
...row,
|
||||
colors: row.colors ? (typeof row.colors === 'string' ? JSON.parse(row.colors) : row.colors) : null,
|
||||
artwork_crop_coords: row.artwork_crop_coords ?
|
||||
(typeof row.artwork_crop_coords === 'string' ? JSON.parse(row.artwork_crop_coords) : row.artwork_crop_coords) : null
|
||||
}))
|
||||
|
||||
return new NextResponse(JSON.stringify(cards), {
|
||||
status: 200,
|
||||
headers: {
|
||||
...headers,
|
||||
|
|
@ -84,7 +108,10 @@ export default async function handler(req: NextRequest) {
|
|||
|
||||
} catch (error) {
|
||||
console.error('API Error:', error)
|
||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
||||
return new NextResponse(JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
...headers,
|
||||
|
|
|
|||
Loading…
Reference in a new issue