Add Neon database integration and migration scripts
- Install @neondatabase/serverless, @vercel/blob, @stackframe/stack - Add database schema setup script (scripts/setup-database.sql) - Add JSON to Neon migration script (scripts/migrate-json-to-neon.ts) - Prepare for user collections, decks, and authentication - Ready for Neon database population with existing card data
This commit is contained in:
parent
df4cd86d90
commit
4fea3d287e
4 changed files with 4327 additions and 0 deletions
3936
package-lock.json
generated
3936
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,8 @@
|
|||
"node": "18.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@stackframe/stack": "^2.8.22",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/blob": "^1.1.1",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"react": "^19.1.0",
|
||||
|
|
|
|||
252
scripts/migrate-json-to-neon.ts
Normal file
252
scripts/migrate-json-to-neon.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
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[];
|
||||
oracle_text?: string;
|
||||
flavor_text?: string;
|
||||
power?: string;
|
||||
toughness?: string;
|
||||
loyalty?: string;
|
||||
artist?: string;
|
||||
image_url?: string;
|
||||
stock_image_url?: string;
|
||||
artwork_crop_coords?: any;
|
||||
current_price?: number;
|
||||
market_price?: number;
|
||||
low_price?: number;
|
||||
high_price?: number;
|
||||
price_last_updated?: string;
|
||||
ocr_confidence?: number;
|
||||
ocr_raw_text?: string;
|
||||
scryfall_id?: string;
|
||||
tcg_player_id?: string;
|
||||
verified: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
async function migrateCards() {
|
||||
console.log('Starting card migration to Neon database...');
|
||||
|
||||
try {
|
||||
// First, ensure the database schema exists
|
||||
console.log('Setting up database schema...');
|
||||
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
set_name VARCHAR(255),
|
||||
set_code VARCHAR(50),
|
||||
card_number VARCHAR(50),
|
||||
rarity VARCHAR(100),
|
||||
game VARCHAR(50) NOT NULL,
|
||||
mana_cost VARCHAR(50),
|
||||
cmc INTEGER,
|
||||
card_type VARCHAR(100),
|
||||
colors JSONB,
|
||||
oracle_text TEXT,
|
||||
flavor_text TEXT,
|
||||
power VARCHAR(50),
|
||||
toughness VARCHAR(50),
|
||||
loyalty VARCHAR(50),
|
||||
artist VARCHAR(255),
|
||||
image_url VARCHAR(500),
|
||||
stock_image_url VARCHAR(500),
|
||||
artwork_crop_coords JSONB,
|
||||
current_price DECIMAL(10,2),
|
||||
market_price DECIMAL(10,2),
|
||||
low_price DECIMAL(10,2),
|
||||
high_price DECIMAL(10,2),
|
||||
price_last_updated TIMESTAMP,
|
||||
ocr_confidence DECIMAL(5,2),
|
||||
ocr_raw_text TEXT,
|
||||
scryfall_id VARCHAR(100),
|
||||
tcg_player_id VARCHAR(100),
|
||||
verified BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
// Create indexes
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_cards_name ON cards(name)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_cards_game ON cards(game)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_cards_set_name ON cards(set_name)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_cards_rarity ON cards(rarity)`;
|
||||
|
||||
console.log('Database schema created successfully');
|
||||
|
||||
// Clear existing data (optional - remove this if you want to keep existing data)
|
||||
await sql`TRUNCATE TABLE cards RESTART IDENTITY`;
|
||||
console.log('Cleared existing card data');
|
||||
|
||||
// Migrate cards from JSON
|
||||
const cards = cardsData as JsonCard[];
|
||||
console.log(`Migrating ${cards.length} cards...`);
|
||||
|
||||
let migratedCount = 0;
|
||||
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}, ${card.set_code}, ${card.card_number},
|
||||
${card.rarity}, ${card.game}, ${card.mana_cost}, ${card.cmc},
|
||||
${card.card_type}, ${JSON.stringify(card.colors)}, ${card.oracle_text},
|
||||
${card.flavor_text}, ${card.power}, ${card.toughness}, ${card.loyalty},
|
||||
${card.artist}, ${card.image_url}, ${card.stock_image_url},
|
||||
${JSON.stringify(card.artwork_crop_coords)}, ${card.current_price},
|
||||
${card.market_price}, ${card.low_price}, ${card.high_price},
|
||||
${card.price_last_updated ? new Date(card.price_last_updated) : null},
|
||||
${card.ocr_confidence}, ${card.ocr_raw_text}, ${card.scryfall_id},
|
||||
${card.tcg_player_id}, ${card.verified},
|
||||
${card.created_at ? new Date(card.created_at) : new Date()},
|
||||
${card.updated_at ? new Date(card.updated_at) : new Date()}
|
||||
)
|
||||
`;
|
||||
migratedCount++;
|
||||
|
||||
if (migratedCount % 10 === 0) {
|
||||
console.log(`Migrated ${migratedCount}/${cards.length} cards...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error migrating card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Successfully migrated ${migratedCount} cards to Neon database!`);
|
||||
|
||||
// Create other tables for collections and decks
|
||||
console.log('Creating user tables...');
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_collections (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS collection_cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
collection_id UUID REFERENCES user_collections(id) ON DELETE CASCADE,
|
||||
card_id INTEGER REFERENCES cards(id),
|
||||
quantity INTEGER DEFAULT 1,
|
||||
condition VARCHAR(50) DEFAULT 'near-mint',
|
||||
notes TEXT,
|
||||
user_images JSONB,
|
||||
purchase_price DECIMAL(10,2),
|
||||
purchase_date DATE,
|
||||
added_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_decks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
game VARCHAR(50) NOT NULL,
|
||||
format VARCHAR(100),
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS deck_cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
deck_id UUID REFERENCES user_decks(id) ON DELETE CASCADE,
|
||||
card_id INTEGER REFERENCES cards(id),
|
||||
quantity INTEGER DEFAULT 1,
|
||||
is_commander BOOLEAN DEFAULT false,
|
||||
is_sideboard BOOLEAN DEFAULT false,
|
||||
added_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
default_view VARCHAR(50) DEFAULT 'card',
|
||||
items_per_page INTEGER DEFAULT 20,
|
||||
enable_animations BOOLEAN DEFAULT true,
|
||||
enable_ocr BOOLEAN DEFAULT true,
|
||||
theme VARCHAR(50) DEFAULT 'light',
|
||||
privacy_settings JSONB DEFAULT '{"collections_public": false, "decks_public": false}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS collection_shares (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
collection_id UUID REFERENCES user_collections(id) ON DELETE CASCADE,
|
||||
shared_with_user_id VARCHAR(255) NOT NULL,
|
||||
permission_level VARCHAR(50) DEFAULT 'view',
|
||||
shared_by_user_id VARCHAR(255) NOT NULL,
|
||||
shared_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
// Create remaining indexes
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_collections_user_id ON user_collections(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_collection_cards_collection_id ON collection_cards(collection_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_collection_cards_card_id ON collection_cards(card_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_decks_user_id ON user_decks(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_deck_cards_deck_id ON deck_cards(deck_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_deck_cards_card_id ON deck_cards(card_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_collection_shares_collection_id ON collection_shares(collection_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_collection_shares_shared_with_user_id ON collection_shares(shared_with_user_id)`;
|
||||
|
||||
console.log('✅ All database tables and indexes created successfully!');
|
||||
console.log('🎉 Migration completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration if this file is executed directly
|
||||
if (require.main === module) {
|
||||
migrateCards().then(() => {
|
||||
console.log('Migration script completed');
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
export { migrateCards };
|
||||
136
scripts/setup-database.sql
Normal file
136
scripts/setup-database.sql
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
-- TCG Vault Database Schema for Neon
|
||||
-- Run this script to set up your database tables
|
||||
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Cards table (migrate from JSON)
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
set_name VARCHAR(255),
|
||||
set_code VARCHAR(50),
|
||||
card_number VARCHAR(50),
|
||||
rarity VARCHAR(100),
|
||||
game VARCHAR(50) NOT NULL,
|
||||
mana_cost VARCHAR(50),
|
||||
cmc INTEGER,
|
||||
card_type VARCHAR(100),
|
||||
colors JSONB,
|
||||
oracle_text TEXT,
|
||||
flavor_text TEXT,
|
||||
power VARCHAR(50),
|
||||
toughness VARCHAR(50),
|
||||
loyalty VARCHAR(50),
|
||||
artist VARCHAR(255),
|
||||
image_url VARCHAR(500),
|
||||
stock_image_url VARCHAR(500),
|
||||
artwork_crop_coords JSONB,
|
||||
current_price DECIMAL(10,2),
|
||||
market_price DECIMAL(10,2),
|
||||
low_price DECIMAL(10,2),
|
||||
high_price DECIMAL(10,2),
|
||||
price_last_updated TIMESTAMP,
|
||||
ocr_confidence DECIMAL(5,2),
|
||||
ocr_raw_text TEXT,
|
||||
scryfall_id VARCHAR(100),
|
||||
tcg_player_id VARCHAR(100),
|
||||
verified BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User collections
|
||||
CREATE TABLE IF NOT EXISTS user_collections (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) NOT NULL, -- Stack Auth user ID
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Collection cards (many-to-many relationship)
|
||||
CREATE TABLE IF NOT EXISTS collection_cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
collection_id UUID REFERENCES user_collections(id) ON DELETE CASCADE,
|
||||
card_id INTEGER REFERENCES cards(id),
|
||||
quantity INTEGER DEFAULT 1,
|
||||
condition VARCHAR(50) DEFAULT 'near-mint',
|
||||
notes TEXT,
|
||||
user_images JSONB, -- Array of Vercel Blob URLs
|
||||
purchase_price DECIMAL(10,2),
|
||||
purchase_date DATE,
|
||||
added_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User decks
|
||||
CREATE TABLE IF NOT EXISTS user_decks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) NOT NULL, -- Stack Auth user ID
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
game VARCHAR(50) NOT NULL,
|
||||
format VARCHAR(100), -- Standard, Modern, Commander, etc.
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Deck cards (many-to-many relationship)
|
||||
CREATE TABLE IF NOT EXISTS deck_cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
deck_id UUID REFERENCES user_decks(id) ON DELETE CASCADE,
|
||||
card_id INTEGER REFERENCES cards(id),
|
||||
quantity INTEGER DEFAULT 1,
|
||||
is_commander BOOLEAN DEFAULT false,
|
||||
is_sideboard BOOLEAN DEFAULT false,
|
||||
added_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User preferences and settings
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) UNIQUE NOT NULL, -- Stack Auth user ID
|
||||
default_view VARCHAR(50) DEFAULT 'card',
|
||||
items_per_page INTEGER DEFAULT 20,
|
||||
enable_animations BOOLEAN DEFAULT true,
|
||||
enable_ocr BOOLEAN DEFAULT true,
|
||||
theme VARCHAR(50) DEFAULT 'light',
|
||||
privacy_settings JSONB DEFAULT '{"collections_public": false, "decks_public": false}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Shared collections (for sharing with friends)
|
||||
CREATE TABLE IF NOT EXISTS collection_shares (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
collection_id UUID REFERENCES user_collections(id) ON DELETE CASCADE,
|
||||
shared_with_user_id VARCHAR(255) NOT NULL, -- Stack Auth user ID
|
||||
permission_level VARCHAR(50) DEFAULT 'view', -- view, edit
|
||||
shared_by_user_id VARCHAR(255) NOT NULL,
|
||||
shared_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_name ON cards(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_game ON cards(game);
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_set_name ON cards(set_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_rarity ON cards(rarity);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_collections_user_id ON user_collections(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_cards_collection_id ON collection_cards(collection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_cards_card_id ON collection_cards(card_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_decks_user_id ON user_decks(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deck_cards_deck_id ON deck_cards(deck_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deck_cards_card_id ON deck_cards(card_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_shares_collection_id ON collection_shares(collection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_shares_shared_with_user_id ON collection_shares(shared_with_user_id);
|
||||
|
||||
-- Insert some sample data (migrate from your current JSON)
|
||||
-- This will be handled by a separate migration script
|
||||
Loading…
Reference in a new issue