From 803f936e2fca5f5abbd25657f020001ac10135cd Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Sun, 29 Mar 2026 20:08:53 -0700 Subject: [PATCH] feat: add PostgreSQL database adapter support --- AGENTS.md | 23 ++++++++ README.md | 57 ++++++++++++++++++- package.json | 5 +- prisma/schema.prisma | 3 +- scripts/seed.js | 113 +++++++++++++++++++++++++++++++++++++ scripts/setup-postgres.js | 113 +++++++++++++++++++++++++++++++++++++ scripts/switch-database.js | 80 ++++++++++++++++++++++++++ src/lib/auth.ts | 5 +- 8 files changed, 393 insertions(+), 6 deletions(-) create mode 100755 scripts/seed.js create mode 100755 scripts/setup-postgres.js create mode 100755 scripts/switch-database.js diff --git a/AGENTS.md b/AGENTS.md index ff68659..7dd4506 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,29 @@ If users are redirected incorrectly or permissions aren't working: 3. Run `npx prisma generate` 4. Update affected API routes and components +### Database Provider Switching +The application supports both SQLite (default) and PostgreSQL databases. + +**To switch between databases:** +```bash +# Switch to SQLite +npm run db:switch sqlite + +# Switch to PostgreSQL +npm run db:switch postgresql +``` + +**To set up PostgreSQL:** +```bash +npm run db:setup-postgres +``` + +**Database Configuration:** +- Edit `.env` file to set `DATABASE_PROVIDER` and `DATABASE_URL` +- For PostgreSQL, also set `DATABASE_SHADOW_URL` for migrations + +**Note:** The database provider is automatically detected by Better Auth and Prisma. + ### Running Tests - **Unit tests**: `npm run test` - **Acceptance tests**: `npm run test:acceptance` diff --git a/README.md b/README.md index 4303174..e030c50 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,14 @@ euchre_camp/ - Node.js 20+ - npm or yarn -- SQLite3 +- **SQLite3** (default) or **PostgreSQL** (optional) + +**For SQLite (default):** +- No additional setup required + +**For PostgreSQL:** +- PostgreSQL 12+ installed and running +- Create a database for EuchreCamp ### Installation @@ -103,13 +110,31 @@ euchre_camp/ npm install ``` -3. **Set up the database** +3. **Set up environment variables** + ```bash + cp .env.example .env + # Edit .env to configure your database + ``` + +4. **Set up the database** + + **For SQLite (default):** ```bash npx prisma migrate deploy npx prisma generate ``` -4. **Start the development server** + **For PostgreSQL:** + ```bash + # Set up PostgreSQL database + npm run db:setup-postgres + + # Or manually: + npx prisma migrate deploy + npx prisma generate + ``` + +5. **Start the development server** ```bash npm run dev ``` @@ -119,11 +144,23 @@ euchre_camp/ Create a `.env` file: ```env +# Database Configuration +DATABASE_PROVIDER=sqlite # or "postgresql" DATABASE_URL="file:./dev.db" + +# Better Auth BETTER_AUTH_SECRET="your-secret-key-here" BETTER_AUTH_URL="http://localhost:3000" ``` +**Using PostgreSQL:** + +```env +DATABASE_PROVIDER=postgresql +DATABASE_URL="postgresql://username:password@localhost:5432/euchre_camp" +DATABASE_SHADOW_URL="postgresql://username:password@localhost:5432/euchre_camp_shadow" +``` + ## Usage ### Creating a Tournament @@ -180,6 +217,20 @@ npm run test npm run test:acceptance ``` +### Database Commands + +```bash +# Switch between SQLite and PostgreSQL +npm run db:switch sqlite +npm run db:switch postgresql + +# Set up PostgreSQL database +npm run db:setup-postgres + +# Seed database with sample data +npm run db:seed +``` + ## Admin User Creation To create an admin user, use the provided scripts: diff --git a/package.json b/package.json index b4da69c..ed15f20 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "test": "vitest", "test:run": "vitest run", "test:acceptance": "playwright test src/__tests__/e2e/", - "test:acceptance:headed": "playwright test src/__tests__/e2e/ --headed" + "test:acceptance:headed": "playwright test src/__tests__/e2e/ --headed", + "db:switch": "node scripts/switch-database.js", + "db:setup-postgres": "node scripts/setup-postgres.js", + "db:seed": "node scripts/seed.js" }, "dependencies": { "@hookform/resolvers": "^5.2.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7ea9e25..16ecc21 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,8 +4,9 @@ generator client { } datasource db { - provider = "sqlite" + provider = env("DATABASE_PROVIDER", "sqlite") url = env("DATABASE_URL") + shadowDatabaseUrl = env("DATABASE_SHADOW_URL") } model Player { diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100755 index 0000000..4c0948d --- /dev/null +++ b/scripts/seed.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/** + * Database seed script for EuchreCamp + * Creates sample data for testing and development + */ + +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Seeding database...\n'); + + // Create admin user if it doesn't exist + const adminEmail = 'david@dhg.lol'; + const existingAdmin = await prisma.user.findUnique({ + where: { email: adminEmail }, + }); + + if (!existingAdmin) { + console.log('Creating admin user...'); + const passwordHash = await bcrypt.hash('adminadmin', 12); + + const adminUser = await prisma.user.create({ + data: { + email: adminEmail, + emailVerified: true, + name: 'David Admin', + role: 'club_admin', + accounts: { + create: { + id: `admin_${Date.now()}`, + accountId: `admin_${Date.now()}`, + providerId: 'credential', + password: passwordHash, + }, + }, + }, + }); + + console.log(`āœ… Created admin user: ${adminUser.email}`); + } else { + console.log('āœ… Admin user already exists'); + } + + // Create sample players if none exist + const playerCount = await prisma.player.count(); + if (playerCount === 0) { + console.log('\nCreating sample players...'); + const samplePlayers = [ + 'John Smith', + 'Jane Doe', + 'Mike Johnson', + 'Sarah Brown', + 'Charlie Wilson', + 'Diana Davis', + 'Robert Taylor', + 'Emily Anderson', + 'David Martinez', + 'Lisa Thompson', + ]; + + for (const name of samplePlayers) { + await prisma.player.create({ + data: { + name, + currentElo: 1000 + Math.floor(Math.random() * 200) - 100, + gamesPlayed: Math.floor(Math.random() * 50), + wins: Math.floor(Math.random() * 30), + losses: Math.floor(Math.random() * 30), + }, + }); + } + console.log(`āœ… Created ${samplePlayers.length} sample players`); + } else { + console.log(`āœ… ${playerCount} players already exist`); + } + + // Create sample tournament + const tournamentCount = await prisma.event.count({ + where: { eventType: 'tournament' }, + }); + + if (tournamentCount === 0) { + console.log('\nCreating sample tournament...'); + const tournament = await prisma.event.create({ + data: { + name: 'Sample Tournament', + eventType: 'tournament', + format: 'round_robin', + eventDate: new Date(), + status: 'planned', + ownerId: existingAdmin?.id, + }, + }); + console.log(`āœ… Created sample tournament: ${tournament.name}`); + } else { + console.log('āœ… Tournaments already exist'); + } + + console.log('\n🌱 Seeding completed successfully!'); +} + +main() + .catch((e) => { + console.error('āŒ Seeding failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/setup-postgres.js b/scripts/setup-postgres.js new file mode 100755 index 0000000..80ca439 --- /dev/null +++ b/scripts/setup-postgres.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/** + * Script to set up PostgreSQL database for EuchreCamp + * This script creates the database and runs migrations + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +function checkPostgresConnection() { + try { + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl || !dbUrl.startsWith('postgresql://')) { + console.error('āŒ DATABASE_URL is not set for PostgreSQL'); + console.error('Please update your .env file with:'); + console.error('DATABASE_PROVIDER=postgresql'); + console.error('DATABASE_URL="postgresql://username:password@localhost:5432/euchre_camp"'); + return false; + } + + // Try to connect using psql + execSync(`psql "${dbUrl}" -c "SELECT 1"`, { stdio: 'pipe' }); + console.log('āœ… PostgreSQL connection successful'); + return true; + } catch (error) { + console.error('āŒ PostgreSQL connection failed:', error.message); + return false; + } +} + +function createDatabase() { + try { + const dbUrl = process.env.DATABASE_URL; + const dbName = dbUrl.split('/').pop(); + + // Create database if it doesn't exist + execSync(`psql "${dbUrl}" -c "CREATE DATABASE ${dbName};" 2>/dev/null || true`, { stdio: 'pipe' }); + console.log(`āœ… Database "${dbName}" ready`); + return true; + } catch (error) { + console.error('āŒ Failed to create database:', error.message); + return false; + } +} + +function runMigrations() { + try { + console.log('šŸ”„ Running migrations...'); + execSync('npx prisma migrate deploy', { stdio: 'inherit' }); + console.log('āœ… Migrations completed successfully'); + return true; + } catch (error) { + console.error('āŒ Migration failed:', error.message); + return false; + } +} + +function generatePrismaClient() { + try { + console.log('šŸ”„ Generating Prisma client...'); + execSync('npx prisma generate', { stdio: 'inherit' }); + console.log('āœ… Prisma client generated successfully'); + return true; + } catch (error) { + console.error('āŒ Prisma client generation failed:', error.message); + return false; + } +} + +function main() { + console.log('=== PostgreSQL Setup for EuchreCamp ===\n'); + + // Check if DATABASE_URL is set + if (!process.env.DATABASE_URL) { + console.error('āŒ DATABASE_URL environment variable is not set'); + console.error('Please add it to your .env file'); + process.exit(1); + } + + // Check PostgreSQL connection + if (!checkPostgresConnection()) { + process.exit(1); + } + + // Create database + if (!createDatabase()) { + process.exit(1); + } + + // Generate Prisma client + if (!generatePrismaClient()) { + process.exit(1); + } + + // Run migrations + if (!runMigrations()) { + process.exit(1); + } + + console.log('\nāœ… PostgreSQL setup completed successfully!'); + console.log('\nNext steps:'); + console.log('1. Start your development server: npm run dev'); + console.log('2. Test the connection by visiting http://localhost:3000'); +} + +// Run if called directly +if (require.main === module) { + main(); +} + +module.exports = { checkPostgresConnection, createDatabase, runMigrations, generatePrismaClient }; diff --git a/scripts/switch-database.js b/scripts/switch-database.js new file mode 100755 index 0000000..7d7aed7 --- /dev/null +++ b/scripts/switch-database.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node + +/** + * Script to switch between SQLite and PostgreSQL databases + * Usage: node scripts/switch-database.js [sqlite|postgres] + */ + +const fs = require('fs'); +const path = require('path'); + +const envFile = '.env'; +const envExampleFile = '.env.example'; + +function updateEnvFile(provider) { + const envPath = path.join(process.cwd(), envFile); + + // Read current .env file or create from example + let envContent = ''; + if (fs.existsSync(envPath)) { + envContent = fs.readFileSync(envPath, 'utf8'); + } else if (fs.existsSync(envExampleFile)) { + envContent = fs.readFileSync(envExampleFile, 'utf8'); + } + + // Update DATABASE_PROVIDER + const providerRegex = /^DATABASE_PROVIDER=.*$/m; + if (providerRegex.test(envContent)) { + envContent = envContent.replace(providerRegex, `DATABASE_PROVIDER=${provider}`); + } else { + envContent += `\nDATABASE_PROVIDER=${provider}\n`; + } + + // Update DATABASE_URL based on provider + if (provider === 'sqlite') { + const sqliteUrl = 'DATABASE_URL="file:./prisma/dev.db"'; + const urlRegex = /^DATABASE_URL=.*$/m; + if (urlRegex.test(envContent)) { + envContent = envContent.replace(urlRegex, sqliteUrl); + } else { + envContent += `${sqliteUrl}\n`; + } + } else if (provider === 'postgresql') { + const pgUrl = 'DATABASE_URL="postgresql://username:password@localhost:5432/euchre_camp"'; + const urlRegex = /^DATABASE_URL=.*$/m; + if (urlRegex.test(envContent)) { + envContent = envContent.replace(urlRegex, pgUrl); + } else { + envContent += `${pgUrl}\n`; + } + } + + // Write updated content + fs.writeFileSync(envPath, envContent); + console.log(`āœ… Updated ${envFile} to use ${provider} database`); +} + +function main() { + const args = process.argv.slice(2); + const provider = args[0]; + + if (!provider) { + console.error('āŒ Usage: node scripts/switch-database.js [sqlite|postgres]'); + process.exit(1); + } + + if (!['sqlite', 'postgres', 'postgresql'].includes(provider)) { + console.error(`āŒ Invalid provider: ${provider}. Must be 'sqlite' or 'postgres'`); + process.exit(1); + } + + const normalizedProvider = provider === 'postgres' ? 'postgresql' : provider; + updateEnvFile(normalizedProvider); + + console.log('\nNext steps:'); + console.log('1. Run: npx prisma generate'); + console.log('2. Run: npx prisma migrate deploy'); + console.log('3. Restart your development server'); +} + +main(); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ceb7716..72ad118 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -3,9 +3,12 @@ import { prismaAdapter } from "better-auth/adapters/prisma"; import { prisma } from "./prisma"; import { testUtils } from "better-auth/plugins"; +// Detect database provider from environment +const databaseProvider = process.env.DATABASE_PROVIDER || "sqlite"; + export const auth = betterAuth({ database: prismaAdapter(prisma, { - provider: "sqlite", + provider: databaseProvider as "sqlite" | "postgresql", }), emailAndPassword: { enabled: true,