deckhearth/api/v1/cards/index.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

122 lines
3.3 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { neon } from '@neondatabase/serverless'
// Initialize Neon client
const sql = neon(process.env.DATABASE_URL!)
interface Card {
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[]
oracle_text?: string
flavor_text?: string
power?: string
toughness?: string
artist?: string
image_url?: string
stock_image_url?: string
artwork_crop_coords?: any
current_price?: number
market_price?: number
verified: boolean
created_at?: string
updated_at?: string
}
export default async function handler(req: NextRequest) {
// Enable CORS
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
if (req.method === 'OPTIONS') {
return new NextResponse(null, { status: 200, headers })
}
try {
const url = new URL(req.url)
const searchParams = url.searchParams
// Get query parameters
const skip = parseInt(searchParams.get('skip') || '0')
const limit = parseInt(searchParams.get('limit') || '100')
const search = searchParams.get('search')
const game = searchParams.get('game')
const set_name = searchParams.get('set_name')
// Build the query dynamically using template literals
let baseQuery = 'SELECT * FROM cards WHERE 1=1'
let queryConditions: string[] = []
let queryValues: any[] = []
if (game) {
queryConditions.push(`game = '${game.replace(/'/g, "''")}'`)
}
if (set_name) {
queryConditions.push(`set_name = '${set_name.replace(/'/g, "''")}'`)
}
if (search) {
const escapedSearch = search.replace(/'/g, "''")
queryConditions.push(`(name ILIKE '%${escapedSearch}%' OR oracle_text ILIKE '%${escapedSearch}%' OR card_type ILIKE '%${escapedSearch}%')`)
}
// Combine conditions
if (queryConditions.length > 0) {
baseQuery += ' AND ' + queryConditions.join(' AND ')
}
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,
'Content-Type': 'application/json',
},
})
} catch (error) {
console.error('API Error:', error)
return new NextResponse(JSON.stringify({
error: 'Internal server error',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
}), {
status: 500,
headers: {
...headers,
'Content-Type': 'application/json',
},
})
}
}