diff --git a/.env.example b/.env.example index ddc3d66..241629c 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,7 @@ DATABASE_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp # Shadow database for Prisma migrations (optional for PostgreSQL) DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp_shadow -# Database provider (postgresql, mysql, sqlite, etc.) +# Database provider (postgresql) DATABASE_PROVIDER=postgresql # ============================================ diff --git a/.gitignore b/.gitignore index 80980e1..6a67180 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,6 @@ next-env.d.ts /src/generated/prisma # database -*.db -*.db-journal -prisma/dev.db* -prisma/prisma/dev.db* playwright-report/ .env.development .env.dev diff --git a/Dockerfile.ci-base b/Dockerfile.ci-base index a9e3753..e8ac883 100644 --- a/Dockerfile.ci-base +++ b/Dockerfile.ci-base @@ -22,8 +22,7 @@ RUN echo "=== Bun Version ===" && bun --version && \ WORKDIR /app # Set default environment variables -ENV DATABASE_PROVIDER=sqlite -ENV DATABASE_URL=file:./prisma/ci.db +ENV DATABASE_PROVIDER=postgresql ENV BETTER_AUTH_SECRET=test-secret-key-for-ci-only ENV NODE_ENV=test diff --git a/e2e/cucumber/step-definitions/auth-steps.ts b/e2e/cucumber/step-definitions/auth-steps.ts index 4f40d3e..5e7a959 100644 --- a/e2e/cucumber/step-definitions/auth-steps.ts +++ b/e2e/cucumber/step-definitions/auth-steps.ts @@ -124,34 +124,67 @@ Given('I am logged in as a tournament admin', async function () { await world.page.fill('input[name="password"]', credentials.password); await world.page.click('button[type="submit"]'); - // Wait for redirect - await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 15000 }); - // Extract user ID from the URL (e.g., /players/2147/profile) + // Wait for any redirect away from register page + await world.page.waitForURL((url) => !url.toString().includes('/auth/register'), { timeout: 15000 }); + await world.page.waitForLoadState('networkidle'); + await world.page.waitForTimeout(1000); + const currentUrl = world.page.url(); + console.log(`šŸŒ After registration, URL: ${currentUrl}`); + + // Try to extract player ID from URL const match = currentUrl.match(/\/players\/(\d+)\/profile/); if (match) { - const playerId = match[1]; - world.playerId = playerId; + world.playerId = match[1]; + console.log(`šŸŒ Player ID from URL: ${world.playerId}`); + } + + // Get the user ID from the database (works regardless of redirect destination) + const prisma = await world.getPrisma(); + console.log(`šŸŒ Looking up user by email: ${credentials.email}`); + const user = await prisma.user.findUnique({ + where: { email: credentials.email }, + include: { player: true } + }); + + if (user) { + (world.user as any).id = user.id; + console.log(`šŸŒ User ID from DB: ${user.id}, role: ${user.role}, playerId: ${user.playerId}`); - // Get the user ID from the database - const prisma = await world.getPrisma(); - const player = await prisma.player.findUnique({ - where: { id: parseInt(playerId) }, - include: { user: true } + if (user.player) { + world.playerId = user.player.id.toString(); + console.log(`šŸŒ Player ID from DB: ${world.playerId}`); + } + + // Assign tournament_admin role + await prisma.user.update({ + where: { id: user.id }, + data: { role: 'tournament_admin' } }); + console.log(`šŸŒ Assigned tournament_admin role to user: ${user.id}`); - if (player && player.user) { - const userId = player.user.id; - (world.user as any).id = userId; - console.log(`šŸŒ User ID extracted: ${userId}`); + // Navigate to trigger a fresh role fetch + await world.page.goto(`${world.baseURL}/rankings`); + await world.page.waitForLoadState('networkidle'); + await world.page.waitForTimeout(500); + } else { + console.log(`šŸŒ WARNING: User not found in DB by email. Trying to find latest user...`); + // Fallback: find the latest user (most recently created) + const latestUser = await prisma.user.findFirst({ + orderBy: { createdAt: 'desc' }, + include: { player: true } + }); + if (latestUser) { + (world.user as any).id = latestUser.id; + world.playerId = latestUser.playerId?.toString() || latestUser.player?.id?.toString(); + console.log(`šŸŒ Using latest user: ${latestUser.id} (${latestUser.email})`); - // Assign tournament_admin role to the user await prisma.user.update({ - where: { id: userId }, + where: { id: latestUser.id }, data: { role: 'tournament_admin' } }); - console.log(`šŸŒ Assigned tournament_admin role to user: ${userId}`); + console.log(`šŸŒ Assigned tournament_admin role`); } } diff --git a/justfile b/justfile index 2f188af..e4cb07f 100644 --- a/justfile +++ b/justfile @@ -22,8 +22,7 @@ IMAGE_TAG_COMMIT := `git rev-parse --short HEAD` # --- Variables --- # Database DB_CONTAINER := "euchre-camp-postgres" -DATABASE_PROVIDER := env_var_or_default("DATABASE_PROVIDER", "sqlite") -DATABASE_URL := env_var_or_default("DATABASE_URL", "file:./prisma/dev.db") +DATABASE_URL := env_var_or_default("DATABASE_URL", "") # --- Setup & Installation --- @@ -32,7 +31,7 @@ setup: @echo "Installing dependencies..." npm install @echo "Setting up database..." - npm run db:setup-postgres + npm run db:setup-dev @echo "Generating Prisma client..." npx prisma generate @@ -56,56 +55,33 @@ format: # --- Testing --- -# Run all tests (unit + acceptance with SQLite) -# Note: Uses Docker containers for consistent environment -test: test-unit test-acceptance-sqlite +# Run all tests (unit + acceptance) +test: test-unit test-acceptance -# Run all tests with PostgreSQL (Docker) -test-pg: test-unit test-acceptance-postgres - -# Run unit tests (Vitest) +# Run unit tests test-unit: npm run test:run -# Run acceptance tests with SQLite (fast, no Docker needed) -test-acceptance-sqlite: - @echo "Running acceptance tests with SQLite..." - DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db BETTER_AUTH_SECRET=test-secret-key npm run test:acceptance - -# Run acceptance tests with PostgreSQL (Docker) -test-acceptance-postgres: - @echo "Starting Docker containers for acceptance tests..." - docker compose up -d - @echo "Waiting for services to be ready..." - sleep 10 +# Run acceptance tests (Playwright) +test-acceptance: @echo "Running acceptance tests..." npm run test:acceptance - @echo "Stopping Docker containers..." - docker compose down -# Run Cucumber e2e tests with SQLite -test-cucumber-sqlite: +# Run Cucumber e2e tests +test-cucumber: @echo "Clearing Next.js cache..." rm -rf .next/ - @echo "Running Cucumber e2e tests with SQLite..." - DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance:cucumber - -# Run Cucumber e2e tests with PostgreSQL (uses .env.development) -test-cucumber-postgres: - @echo "Clearing Next.js cache..." - rm -rf .next/ - @echo "Running Cucumber e2e tests with PostgreSQL..." + @echo "Running Cucumber e2e tests..." npm run test:acceptance:cucumber -# Run Cucumber e2e tests with PostgreSQL against production build -# This is more reliable than dev server (no HMR, faster API responses) -test-cucumber-postgres-prod: +# Run Cucumber e2e tests against production build +test-cucumber-prod: @echo "Clearing Next.js cache..." rm -rf .next/ @echo "Building application for production..." bun run build @echo "Starting production server in background..." - DATABASE_URL=$(grep DATABASE_URL .env.development | cut -d'=' -f2 | tr -d '"') DATABASE_PROVIDER=postgresql bun run start > /tmp/next-prod.log 2>&1 & + bun run start > /tmp/next-prod.log 2>&1 & SERVER_PID=$$! @echo "Waiting for server to be ready..." sleep 15 @@ -115,9 +91,6 @@ test-cucumber-postgres-prod: kill $$SERVER_PID 2>/dev/null || true @echo "Tests completed." -# Run all e2e tests (both Playwright and Cucumber) -test-e2e: test-acceptance-sqlite test-cucumber-sqlite - # Run database migrations (Prisma) migrate: npx prisma migrate dev @@ -126,13 +99,6 @@ migrate: seed: npm run db:seed -# Switch database provider -db-switch-sqlite: - npm run db:switch sqlite - -db-switch-postgres: - npm run db:switch postgresql - # --- Docker --- # Build the Docker image (standard build) @@ -148,7 +114,6 @@ docker-build-commit: docker-build-full: docker-build docker-build-commit # Fast rebuild using Docker BuildKit cache -# Uses build cache to speed up rebuilds when only code changes docker-rebuild-fast: @echo "Fast rebuilding Docker image {{PROJECT}}:{{IMAGE_TAG}}..." DOCKER_BUILDKIT=1 docker build \ @@ -195,19 +160,14 @@ docker-push: docker-build-full # --- CI/CD Pipeline Simulation --- -# Run full CI pipeline locally (lint, test, build, push) -# Matches the Gitea Actions workflow -ci: lint typecheck test-unit test-acceptance-sqlite docker-build +# Run full CI pipeline locally (lint, test, build) +ci: lint typecheck test-unit test-acceptance docker-build @echo "CI Pipeline completed successfully!" # PR validation (what runs on pull requests) -pr-validate: lint typecheck test-unit test-acceptance-sqlite +pr-validate: lint typecheck test-unit test-acceptance @echo "PR validation completed successfully!" -# Run CI with PostgreSQL (for release workflow simulation) -ci-postgres: lint typecheck test-unit test-acceptance-postgres docker-build - @echo "CI Pipeline with PostgreSQL completed successfully!" - # --- Utilities --- # Show help information @@ -279,13 +239,3 @@ workflow-status: @echo "PR Workflow: Runs unit + acceptance tests on pull requests" @echo "Test Workflow: Runs unit tests on all branch pushes" @echo "Release Workflow: Runs on main branch pushes (version bump + Docker build)" - @echo "" - @echo "Note: CI image approach deprecated due to Gitea Actions workspace mounting" - -# Check current database provider -db-status: - @echo "Database Provider: ${DATABASE_PROVIDER}" - @echo "Database URL: ${DATABASE_URL}" - @echo "" - @echo "Current schema.prisma provider:" - grep -A 2 "datasource db" prisma/schema.prisma | head -3 diff --git a/package.json b/package.json index 21c3eb7..4170b15 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,8 @@ "test:acceptance:cucumber:pretty": "DATABASE_URL=$(grep DATABASE_URL .env.development | cut -d'=' -f2 | tr -d '\"') DATABASE_PROVIDER=postgresql bun cucumber-js --config e2e/cucumber/cucumber.config.ts --format pretty:cucumber-pretty", "test:acceptance:cucumber:prod": "bun run build && (trap 'kill $(jobs -p) 2>/dev/null || true' EXIT; DATABASE_URL=${DATABASE_URL:-$(grep DATABASE_URL .env.development | cut -d'=' -f2 | tr -d '\"')} DATABASE_PROVIDER=${DATABASE_PROVIDER:-postgresql} bun run start & echo 'Waiting for server to start...'; for i in {1..30}; do if curl -s http://localhost:3000 > /dev/null 2>&1; then echo 'Server ready!'; break; fi; sleep 1; done; npm run test:acceptance:cucumber)", "cucumber": "DATABASE_URL=$(grep DATABASE_URL .env.development | cut -d'=' -f2 | tr -d '\"') DATABASE_PROVIDER=postgresql bun cucumber-js --config e2e/cucumber/cucumber.config.ts", - "db:switch": "bun run scripts/switch-database.js", - "db:setup-postgres": "bun run scripts/setup-postgres.js", "db:setup-dev": "bun run scripts/setup-postgres.js", "db:setup-dev:clean": "bun run scripts/setup-postgres.js --drop", - "db:reset-dev": "bun run scripts/reset-dev-db.js", - "db:use-dev": "bun run scripts/use-dev-db.js", "db:cleanup-prod": "bun run scripts/cleanup-prod-db.js", "db:check-prod": "bun run scripts/check-test-records.js", "db:seed": "bun run scripts/seed.js", diff --git a/playwright.config.ts b/playwright.config.ts index 7579fdc..e15df46 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,13 +6,13 @@ export default defineConfig({ expect: { timeout: 5000 }, - // Run tests sequentially to avoid database conflicts with SQLite + // Run tests sequentially to avoid database conflicts fullyParallel: false, // Fail the build on CI if you accidentally left test.only in the source code. forbidOnly: !!process.env.CI, // Retry on CI only. retries: process.env.CI ? 2 : 0, - // Always run with 1 worker to avoid database conflicts with SQLite + // Always run with 1 worker to avoid database conflicts workers: 1, // Reporter to use reporter: 'html', diff --git a/scripts/python/generate_games.py b/scripts/python/generate_games.py deleted file mode 100644 index df38ad6..0000000 --- a/scripts/python/generate_games.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate 100+ games to test ELO rating calculations -""" - -import sqlite3 -import random -from datetime import datetime, timedelta - -DB_PATH = "prisma/prisma/dev.db" - - -def get_players(): - """Get all players from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute("SELECT id, name, currentElo FROM players") - players = cursor.fetchall() - conn.close() - return players - - -def generate_game(players, game_num, base_date): - """Generate a single game with random players and scores""" - # Randomly select 4 different players - selected_players = random.sample(players, 4) - p1, p2, p3, p4 = selected_players - - # Randomly assign teams (Team 1 vs Team 2) - team1_p1 = p1 - team1_p2 = p2 - team2_p1 = p3 - team2_p2 = p4 - - # Generate realistic scores (Euchre games typically 10 points max) - # Team 1 wins 60% of the time for variety - team1_wins = random.random() < 0.6 - if team1_wins: - # Team 1 wins - generate scores - team1_score = random.randint(10, 15) - team2_score = random.randint(0, 9) - else: - # Team 2 wins - generate scores - team2_score = random.randint(10, 15) - team1_score = random.randint(0, 9) - - # Generate random date in the past 30 days - days_ago = random.randint(0, 30) - game_date = base_date - timedelta( - days=days_ago, hours=random.randint(0, 23), minutes=random.randint(0, 59) - ) - - return { - "team1P1Id": team1_p1[0], - "team1P2Id": team1_p2[0], - "team2P1Id": team2_p1[0], - "team2P2Id": team2_p2[0], - "team1Score": team1_score, - "team2Score": team2_score, - "playedAt": game_date.isoformat() + "Z", - "status": "completed", - "eventId": None, - } - - -def insert_games(games): - """Insert games into the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - for game in games: - cursor.execute( - """ - INSERT INTO matches - (team1P1Id, team1P2Id, team2P1Id, team2P2Id, team1Score, team2Score, playedAt, status, eventId, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - game["team1P1Id"], - game["team1P2Id"], - game["team2P1Id"], - game["team2P2Id"], - game["team1Score"], - game["team2Score"], - game["playedAt"], - game["status"], - game["eventId"], - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - ), - ) - - conn.commit() - conn.close() - - -def main(): - print("Generating 150 games to test ELO ratings...") - print("=" * 60) - - players = get_players() - print(f"Found {len(players)} players in database") - - # Generate 150 games - num_games = 150 - base_date = datetime.now() - - games = [] - for i in range(num_games): - game = generate_game(players, i, base_date) - games.append(game) - - print(f"Generated {len(games)} games") - - # Insert games into database - print("Inserting games into database...") - insert_games(games) - print("Games inserted successfully!") - - # Show sample games - print("\nSample games:") - print("-" * 80) - for i, game in enumerate(games[:3]): - print( - f"Game {i + 1}: Team 1 ({game['team1Score']}) vs Team 2 ({game['team2Score']})" - ) - print(f" Team 1: Player {game['team1P1Id']} + Player {game['team1P2Id']}") - print(f" Team 2: Player {game['team2P1Id']} + Player {game['team2P2Id']}") - print(f" Date: {game['playedAt']}") - print() - - print("=" * 60) - print(f"Total games generated: {num_games}") - print("Now check the ELO ratings by running the application!") - - -if __name__ == "__main__": - main() diff --git a/scripts/python/update_partnership_stats.py b/scripts/python/update_partnership_stats.py deleted file mode 100644 index 7d9f82a..0000000 --- a/scripts/python/update_partnership_stats.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -""" -Update partnership statistics based on matches in the database -""" - -import sqlite3 -import math -from datetime import datetime - -DB_PATH = "prisma/prisma/dev.db" -K_FACTOR = 32 - - -def get_all_matches(): - """Get all matches from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, - team1Score, team2Score, playedAt - FROM matches - ORDER BY playedAt - """) - matches = cursor.fetchall() - conn.close() - return matches - - -def get_or_create_partnership(player1_id, player2_id): - """Get or create a partnership record""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Sort IDs to ensure consistent partnership lookup - p1 = min(player1_id, player2_id) - p2 = max(player1_id, player2_id) - - cursor.execute( - """ - SELECT id, gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? AND player2Id = ? - """, - (p1, p2), - ) - - result = cursor.fetchone() - if result: - conn.close() - return result - - # Create new partnership record - cursor.execute( - """ - INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) - VALUES (?, ?, 0, 0, 0, 0, NULL, ?, ?) - """, - (p1, p2, datetime.now().isoformat() + "Z", datetime.now().isoformat() + "Z"), - ) - - partnership_id = cursor.lastrowid - conn.commit() - conn.close() - - return (partnership_id, 0, 0, 0, 0) - - -def update_partnership_stats(player1_id, player2_id, won, elo_change): - """Update partnership statistics""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Sort IDs to ensure consistent partnership lookup - p1 = min(player1_id, player2_id) - p2 = max(player1_id, player2_id) - - # Get current partnership stats - cursor.execute( - """ - SELECT id, gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? AND player2Id = ? - """, - (p1, p2), - ) - - result = cursor.fetchone() - if not result: - # Create new partnership record - cursor.execute( - """ - INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) - VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) - """, - ( - p1, - p2, - 1 if won else 0, - 0 if won else 1, - elo_change, - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - ), - ) - else: - partnership_id, games_played, wins, losses, total_elo_change = result - - # Update partnership stats - new_games = games_played + 1 - new_wins = wins + 1 if won else wins - new_losses = losses if won else losses + 1 - new_total_elo_change = total_elo_change + elo_change - - cursor.execute( - """ - UPDATE partnership_stats - SET gamesPlayed = ?, wins = ?, losses = ?, totalEloChange = ?, lastPlayed = ?, updatedAt = ? - WHERE id = ? - """, - ( - new_games, - new_wins, - new_losses, - new_total_elo_change, - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - partnership_id, - ), - ) - - conn.commit() - conn.close() - - -def main(): - print("Updating partnership statistics based on matches...") - print("=" * 60) - - # Get all matches - matches = get_all_matches() - print(f"Found {len(matches)} matches in database") - - # Reset partnership stats - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute("DELETE FROM partnership_stats") - conn.commit() - conn.close() - print("Reset all partnership statistics") - - # Process each match - match_count = 0 - for ( - match_id, - team1_p1, - team1_p2, - team2_p1, - team2_p2, - team1_score, - team2_score, - played_at, - ) in matches: - # Determine winners - team1_won = team1_score > team2_score - team2_won = team2_score > team1_score - - # Update partnership stats for Team 1 - if team1_won: - update_partnership_stats( - team1_p1, team1_p2, True, 0 - ) # Elo change will be calculated separately - else: - update_partnership_stats(team1_p1, team1_p2, False, 0) - - # Update partnership stats for Team 2 - if team2_won: - update_partnership_stats(team2_p1, team2_p2, True, 0) - else: - update_partnership_stats(team2_p1, team2_p2, False, 0) - - match_count += 1 - if match_count % 20 == 0: - print(f"Processed {match_count}/{len(matches)} matches...") - - print(f"Processed {match_count} matches") - - # Display partnership stats for top players - print("\n" + "=" * 60) - print("Partnership Stats for Top Players:") - print("-" * 60) - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Get top players by ELO - cursor.execute("SELECT id, name FROM players ORDER BY currentElo DESC LIMIT 5") - top_players = cursor.fetchall() - - for player_id, player_name in top_players: - print(f"\n{player_name}:") - - # Get partnership stats for this player - cursor.execute( - """ - SELECT - CASE - WHEN player1Id = ? THEN (SELECT name FROM players WHERE id = player2Id) - ELSE (SELECT name FROM players WHERE id = player1Id) - END as partner_name, - gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? OR player2Id = ? - ORDER BY gamesPlayed DESC - LIMIT 3 - """, - (player_id, player_id, player_id), - ) - - partnerships = cursor.fetchall() - for partner_name, games, wins, losses, elo_change in partnerships: - win_rate = (wins / games * 100) if games > 0 else 0 - print( - f" - with {partner_name}: {games} games, {wins}/{losses} ({win_rate:.1f}%) ELO: {elo_change:+d}" - ) - - conn.close() - - print("\n" + "=" * 60) - print("Partnership statistics updated successfully!") - - -if __name__ == "__main__": - main() diff --git a/scripts/python/update_player_stats.py b/scripts/python/update_player_stats.py deleted file mode 100644 index b2525da..0000000 --- a/scripts/python/update_player_stats.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 -""" -Update player statistics (ELO, gamesPlayed, wins, losses) based on matches in the database -""" - -import sqlite3 -import math - -DB_PATH = "prisma/prisma/dev.db" -K_FACTOR = 32 # Standard K-factor for Elo calculations - - -def get_all_matches(): - """Get all matches from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, - team1Score, team2Score, playedAt - FROM matches - ORDER BY playedAt - """) - matches = cursor.fetchall() - conn.close() - return matches - - -def get_player(player_id): - """Get a player by ID""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - "SELECT id, name, currentElo, gamesPlayed, wins, losses FROM players WHERE id = ?", - (player_id,), - ) - player = cursor.fetchone() - conn.close() - return player - - -def update_player(player_id, current_elo, games_played, wins, losses): - """Update a player's statistics""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - """ - UPDATE players - SET currentElo = ?, gamesPlayed = ?, wins = ?, losses = ? - WHERE id = ? - """, - (current_elo, games_played, wins, losses, player_id), - ) - conn.commit() - conn.close() - - -def calculate_elo_change(rating_a, rating_b, score_a, score_b): - """Calculate Elo change for a match""" - # Calculate expected scores - expected_a = 1 / (1 + math.pow(10, (rating_b - rating_a) / 400)) - expected_b = 1 - expected_a - - # Actual scores (1 for win, 0.5 for tie, 0 for loss) - actual_a = 0.5 if score_a == score_b else (1 if score_a > score_b else 0) - actual_b = 0.5 if score_a == score_b else (1 if score_b > score_a else 0) - - # Calculate Elo change - elo_change_a = K_FACTOR * (actual_a - expected_a) - elo_change_b = K_FACTOR * (actual_b - expected_b) - - return elo_change_a, elo_change_b - - -def main(): - print("Updating player statistics based on matches in database...") - print("=" * 60) - - # Get all matches - matches = get_all_matches() - print(f"Found {len(matches)} matches in database") - - # Reset all player stats to 0 before recalculating - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - "UPDATE players SET currentElo = 1000, gamesPlayed = 0, wins = 0, losses = 0" - ) - conn.commit() - conn.close() - print("Reset all player statistics to initial values (ELO: 1000, games: 0)") - - # Process each match - match_count = 0 - for ( - match_id, - team1_p1, - team1_p2, - team2_p1, - team2_p2, - team1_score, - team2_score, - played_at, - ) in matches: - # Get player data - p1 = get_player(team1_p1) - p2 = get_player(team1_p2) - p3 = get_player(team2_p1) - p4 = get_player(team2_p2) - - if not all([p1, p2, p3, p4]): - print(f"Warning: Could not find all players for match {match_id}") - continue - - # Calculate team ratings - team1_rating = (p1[2] + p2[2]) / 2 # currentElo - team2_rating = (p3[2] + p4[2]) / 2 # currentElo - - # Calculate Elo changes - team1_elo_change, team2_elo_change = calculate_elo_change( - team1_rating, team2_rating, team1_score, team2_score - ) - - # Individual Elo changes (split evenly between team members) - p1_elo_change = team1_elo_change / 2 - p2_elo_change = team1_elo_change / 2 - p3_elo_change = team2_elo_change / 2 - p4_elo_change = team2_elo_change / 2 - - # Determine winners - team1_won = team1_score > team2_score - team2_won = team2_score > team1_score - - # Update player 1 stats - p1_new_elo = int(p1[2] + p1_elo_change) - p1_new_games = p1[3] + 1 - p1_new_wins = p1[4] + 1 if team1_won else p1[4] - p1_new_losses = p1[5] if team1_won else p1[5] + 1 - update_player(p1[0], p1_new_elo, p1_new_games, p1_new_wins, p1_new_losses) - - # Update player 2 stats - p2_new_elo = int(p2[2] + p2_elo_change) - p2_new_games = p2[3] + 1 - p2_new_wins = p2[4] + 1 if team1_won else p2[4] - p2_new_losses = p2[5] if team1_won else p2[5] + 1 - update_player(p2[0], p2_new_elo, p2_new_games, p2_new_wins, p2_new_losses) - - # Update player 3 stats - p3_new_elo = int(p3[2] + p3_elo_change) - p3_new_games = p3[3] + 1 - p3_new_wins = p3[4] + 1 if team2_won else p3[4] - p3_new_losses = p3[5] if team2_won else p3[5] + 1 - update_player(p3[0], p3_new_elo, p3_new_games, p3_new_wins, p3_new_losses) - - # Update player 4 stats - p4_new_elo = int(p4[2] + p4_elo_change) - p4_new_games = p4[3] + 1 - p4_new_wins = p4[4] + 1 if team2_won else p4[4] - p4_new_losses = p4[5] if team2_won else p4[5] + 1 - update_player(p4[0], p4_new_elo, p4_new_games, p4_new_wins, p4_new_losses) - - match_count += 1 - if match_count % 20 == 0: - print(f"Processed {match_count}/{len(matches)} matches...") - - print(f"Processed {match_count} matches") - - # Display updated player rankings - print("\n" + "=" * 60) - print("Top 10 Players by ELO Rating:") - print("-" * 60) - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, name, currentElo, gamesPlayed, wins, losses - FROM players - ORDER BY currentElo DESC - LIMIT 10 - """) - top_players = cursor.fetchall() - conn.close() - - for rank, (player_id, name, elo, games, wins, losses) in enumerate(top_players, 1): - win_rate = (wins / games * 100) if games > 0 else 0 - print( - f"{rank:2}. {name:15} | ELO: {elo:4} | Games: {games:3} | W/L: {wins}/{losses} ({win_rate:.1f}%)" - ) - - print("\n" + "=" * 60) - print("Player statistics updated successfully!") - print("Now run the application to see updated rankings.") - - -if __name__ == "__main__": - main() diff --git a/scripts/switch-database.js b/scripts/switch-database.js deleted file mode 100755 index e36a585..0000000 --- a/scripts/switch-database.js +++ /dev/null @@ -1,108 +0,0 @@ -#!/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 (note: this is for reference only, not used by Prisma) - 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 updateSchemaFile(provider) { - const schemaPath = path.join(process.cwd(), 'prisma', 'schema.prisma'); - - if (!fs.existsSync(schemaPath)) { - console.error(`āŒ Schema file not found: ${schemaPath}`); - return false; - } - - let schemaContent = fs.readFileSync(schemaPath, 'utf8'); - - // Update the provider in the datasource block - const providerRegex = /(datasource db\s*\{[^}]*provider\s*=\s*)"[^"]+"/; - if (providerRegex.test(schemaContent)) { - schemaContent = schemaContent.replace( - providerRegex, - `$1"${provider}"` - ); - - fs.writeFileSync(schemaPath, schemaContent); - console.log(`āœ… Updated schema.prisma to use ${provider} provider`); - return true; - } else { - console.error(`āŒ Could not find provider declaration in schema.prisma`); - return false; - } -} - -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); - updateSchemaFile(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/scripts/use-dev-db.js b/scripts/use-dev-db.js deleted file mode 100755 index 30f46a3..0000000 --- a/scripts/use-dev-db.js +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node - -/** - * Switch to development database - * Creates a .env.development.local file with development database settings - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -const envDevPath = path.join(__dirname, '..', '.env.development'); -const envDevLocalPath = path.join(__dirname, '..', '.env.development.local'); - -console.log('šŸ”§ Setting up development database...\n'); - -// Check if .env.development exists -if (!fs.existsSync(envDevPath)) { - console.error('āŒ .env.development file not found'); - console.error('Please create it first or run: npm run db:setup-dev'); - process.exit(1); -} - -// Copy .env.development to .env.development.local if it doesn't exist -if (!fs.existsSync(envDevLocalPath)) { - fs.copyFileSync(envDevPath, envDevLocalPath); - console.log('āœ… Created .env.development.local'); -} else { - console.log('ā„¹ļø .env.development.local already exists'); -} - -// Set NODE_ENV for the current session -process.env.NODE_ENV = 'development'; -process.env.DATABASE_PROVIDER = 'postgresql'; - -// Read the development database URL -const envContent = fs.readFileSync(envDevPath, 'utf8'); -const match = envContent.match(/DATABASE_URL="([^"]+)"/); -if (match) { - process.env.DATABASE_URL = match[1]; - console.log(`āœ… Development database URL: ${process.env.DATABASE_URL}`); -} - -console.log('\nāœ… Development database configured!'); -console.log('\nNext steps:'); -console.log('1. Setup the dev database: npm run db:setup-dev'); -console.log('2. Start development server: npm run dev'); -console.log('\nNote: This script sets environment variables for the current session.'); -console.log('For persistent configuration, use .env.development.local'); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 0a8d6a2..ea04554 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -3,42 +3,34 @@ 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: databaseProvider as "sqlite" | "postgresql", + provider: "postgresql", }), emailAndPassword: { enabled: true, - autoSignIn: true, // Automatically sign in after registration - requireEmailVerification: false, // Don't require email verification for tests - minPasswordLength: 8, // Set minimum password length - maxPasswordLength: 128, // Set maximum password length + autoSignIn: true, + requireEmailVerification: false, + minPasswordLength: 8, + maxPasswordLength: 128, }, secret: process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET, baseURL: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL || "http://localhost:3000/api/auth", - // Configure trusted origins - parse from environment or use defaults trustedOrigins: (() => { const origins = []; - - // Add environment-specified origins + if (process.env.TRUSTED_ORIGINS) { origins.push(...process.env.TRUSTED_ORIGINS.split(',').map(o => o.trim())); } - - // Add BETTER_AUTH_URL if set + if (process.env.BETTER_AUTH_URL) { origins.push(process.env.BETTER_AUTH_URL); } - - // Add NEXTAUTH_URL if set + if (process.env.NEXTAUTH_URL) { origins.push(process.env.NEXTAUTH_URL); } - - // Add defaults + origins.push( "https://euchre.notsosm.art", "http://euchre.notsosm.art", @@ -47,17 +39,14 @@ export const auth = betterAuth({ "http://127.0.0.1:3000", "http://0.0.0.0:3000" ); - - // Remove duplicates and empty strings + return [...new Set(origins.filter(o => o))]; })(), session: { cookieCache: { - enabled: false, // Disable cookie cache to avoid session cache issues + enabled: false, }, }, - // Configure rate limiting - disable for test environment - // Note: Rate limiting is disabled for all environments to ensure test reliability rateLimit: { enabled: false, }, @@ -66,21 +55,18 @@ export const auth = betterAuth({ user: { create: { async after(user) { - // Generate a unique player name using timestamp and random string const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 8); const baseName = user.name || user.email.split('@')[0]; const uniqueName = `${baseName}-${timestamp}-${randomId}`; - - // Create a Player record for the new user + const newPlayer = await prisma.player.create({ data: { name: uniqueName, normalizedName: uniqueName.toLowerCase(), }, }); - - // Update the User with the playerId + await prisma.user.update({ where: { id: user.id }, data: { playerId: newPlayer.id }, @@ -93,4 +79,4 @@ export const auth = betterAuth({ plugins: [ testUtils() ] -}); \ No newline at end of file +}); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 57a164b..49169ed 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,37 +1,19 @@ import { PrismaClient } from '@prisma/client' +import { PrismaPg } from '@prisma/adapter-pg' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } -// Detect database provider from environment (default to sqlite for local development) -// Next.js automatically loads environment variables from .env, .env.development, .env.production -const databaseProvider = process.env.DATABASE_PROVIDER || 'sqlite' const databaseUrl = process.env.DATABASE_URL -// Create PrismaClient with appropriate adapter +if (!databaseUrl) { + throw new Error('DATABASE_URL environment variable is required.') +} + const createPrismaClient = () => { - let client: PrismaClient - - if (databaseProvider === 'postgresql') { - // Validate DATABASE_URL is present for PostgreSQL - if (!databaseUrl) { - throw new Error( - 'DATABASE_URL environment variable is required when DATABASE_PROVIDER is set to postgresql. ' + - 'Current DATABASE_PROVIDER: ' + databaseProvider - ) - } - - // Use PrismaPg adapter for PostgreSQL - const { PrismaPg } = require('@prisma/adapter-pg') - const adapter = new PrismaPg({ connectionString: databaseUrl }) - client = new PrismaClient({ adapter }) - } else { - // No adapter needed for SQLite - client = new PrismaClient() - } - - return client + const adapter = new PrismaPg({ connectionString: databaseUrl }) + return new PrismaClient({ adapter }) } export const prisma = globalForPrisma.prisma ?? createPrismaClient()