feat: add PostgreSQL database adapter support

This commit is contained in:
2026-03-29 20:08:53 -07:00
parent 54fd6abb5e
commit 803f936e2f
8 changed files with 393 additions and 6 deletions
+23
View File
@@ -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`
+54 -3
View File
@@ -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:
+4 -1
View File
@@ -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",
+2 -1
View File
@@ -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 {
+113
View File
@@ -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();
});
+113
View File
@@ -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 };
+80
View File
@@ -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();
+4 -1
View File
@@ -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,