Migrate database from MySQL to Supabase PostgreSQL

- Added @supabase/supabase-js client library
- Rewrote database.js to use Supabase API
- Updated server.js health check for Supabase
- Updated db.config.example.js with Supabase format
- Created comprehensive SUPABASE_SETUP.md guide
- Added SQL schema files for easy deployment
- Updated README_DB_CONFIG.md for Supabase

Benefits:
- Managed PostgreSQL database
- Built-in Row Level Security
- Real-time capabilities
- Easy monitoring via dashboard
- Free tier for development
This commit is contained in:
2025-12-21 15:40:57 +11:00
parent 5238fc8d22
commit 054cbf3e77
9 changed files with 872 additions and 276 deletions

View File

@@ -1,125 +1,86 @@
# Database Configuration Setup # Database Configuration Setup - Supabase
## Overview ## Overview
Database credentials are stored in a separate configuration file (`db.config.js`) that is **NOT committed to GitHub** for security reasons. Database credentials are stored in a separate configuration file (`db.config.js`) that is **NOT committed to GitHub** for security reasons.
This project now uses **Supabase** (PostgreSQL) instead of MySQL.
## Files ## Files
### 1. `db.config.example.js` (Committed to Git) ### 1. `db.config.example.js` (Committed to Git)
Template file showing the required configuration structure. Template file showing the required Supabase configuration structure.
### 2. `db.config.js` (NOT Committed - in .gitignore) ### 2. `db.config.js` (NOT Committed - in .gitignore)
Contains actual database credentials. This file must be created manually. Contains actual Supabase credentials. This file must be created manually.
### 3. `.gitignore` ### 3. `.gitignore`
Ensures `db.config.js` is never committed to the repository. Ensures `db.config.js` is never committed to the repository.
## Setup Instructions ## Quick Setup
### For Local Development See **[SUPABASE_SETUP.md](SUPABASE_SETUP.md)** for detailed step-by-step instructions.
1. **Copy the example file:** ### Quick Start
```bash
cp db.config.example.js db.config.js
```
2. **Edit `db.config.js` with your credentials:** 1. **Create Supabase project** at [app.supabase.com](https://app.supabase.com)
2. **Copy credentials** from Project Settings → API
3. **Update `db.config.js`:**
```javascript ```javascript
module.exports = { module.exports = {
host: 'localhost', // or your database host supabaseUrl: 'https://xxxxx.supabase.co',
user: 'your_username', supabaseAnonKey: 'eyJhbGci...',
password: 'your_password', supabasePassword: 't1hWsackxbYzRIPD'
database: 'appgconnect5_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
}; };
``` ```
4. **Run SQL schema** in Supabase SQL Editor (see SUPABASE_SETUP.md)
3. **Start the server:** 5. **Start server:** `npm start`
```bash
npm start
```
### For Production Deployment
1. **Pull the latest code on your server:**
```bash
git pull origin main
```
2. **Create `db.config.js` on the production server:**
```bash
nano db.config.js
# or
vi db.config.js
```
3. **Add your production database credentials:**
```javascript
module.exports = {
host: 'your-production-db-host.com',
user: 'production_user',
password: 'secure_production_password',
database: 'appgconnect5_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
};
```
4. **Save and restart the server:**
```bash
pm2 restart connect5
# or your restart command
```
## Security Features ## Security Features
✅ **Credentials not in git** - `db.config.js` is in `.gitignore` ✅ **Credentials not in git** - `db.config.js` is in `.gitignore`
✅ **Template provided** - `db.config.example.js` shows the structure ✅ **Template provided** - `db.config.example.js` shows the structure
✅ **Comments in code** - Clear instructions in `database.js` ✅ **Supabase RLS** - Row Level Security policies protect data
✅ **Separate config** - Easy to update without touching main code ✅ **Separate config** - Easy to update without touching main code
## Troubleshooting ## Troubleshooting
### Error: Cannot find module './db.config.js' ### Error: Cannot find module './db.config.js'
**Solution:** You need to create the `db.config.js` file: **Solution:** Create the `db.config.js` file:
```bash ```bash
cp db.config.example.js db.config.js cp db.config.example.js db.config.js
# Then edit with your credentials # Then edit with your Supabase credentials
``` ```
### Error: Access denied for user ### Error: Invalid API key
**Solution:** Check your credentials in `db.config.js`: **Solution:** Check your credentials in `db.config.js`:
- Verify username - Verify `supabaseUrl` is correct
- Verify password - Verify `supabaseAnonKey` (should start with `eyJ...`)
- Check host address - Get credentials from Supabase dashboard → Project Settings → API
- Ensure user has proper permissions
### Connection timeout ### Error: Table 'players' does not exist
**Solution:** **Solution:**
- Check if MySQL server is running - Run the SQL schema in Supabase SQL Editor
- Verify firewall allows connection - See SUPABASE_SETUP.md Step 4 for the complete schema
- Check host address is correct
## Important Notes ## Important Notes
⚠️ **NEVER commit `db.config.js` to git** ⚠️ **NEVER commit `db.config.js` to git**
⚠️ **Keep production credentials secure** ⚠️ **Keep credentials secure**
⚠️ **Use different credentials for dev/prod** ⚠️ **Use different projects for dev/prod**
⚠️ **Regularly rotate passwords** ⚠️ **The anon key is safe for client-side use** (protected by RLS)
## File Structure ## File Structure
``` ```
Connect-5/ Connect-5/
├── db.config.example.js ← Template (in git) ├── db.config.example.js ← Template (in git)
├── db.config.js ← Your credentials (NOT in git) ├── db.config.js ← Your credentials (NOT in git)
├── .gitignore ← Protects db.config.js ├── .gitignore ← Protects db.config.js
├── database.js ← Imports from db.config.js ├── database.js ← Imports from db.config.js
── README_DB_CONFIG.md ← This file ── supabase-functions.sql ← Helper functions for Supabase
├── SUPABASE_SETUP.md ← Detailed setup guide
└── README_DB_CONFIG.md ← This file
``` ```

223
SUPABASE_SETUP.md Normal file
View File

@@ -0,0 +1,223 @@
# Supabase Setup Guide for Connect-5
This guide will help you set up Supabase for the Connect-5 multiplayer game.
## Step 1: Create Supabase Project
1. Go to [https://app.supabase.com](https://app.supabase.com)
2. Sign in or create an account
3. Click "New Project"
4. Fill in the project details:
- **Organization**: Select or create your organization (e.g., "DeNNiiInc's Org")
- **Project name**: `Connect5`
- **Database password**: `t1hWsackxbYzRIPD` (or your chosen password)
- **Region**: Oceania (Sydney) - or closest to your users
- **Pricing Plan**: Free tier is sufficient for development
5. Click "Create new project"
6. Wait for the project to be provisioned (takes 1-2 minutes)
## Step 2: Get Your Credentials
Once your project is ready:
1. Go to **Project Settings** (gear icon in sidebar)
2. Navigate to **API** section
3. Copy the following values:
- **Project URL** (e.g., `https://xxxxxxxxxxxxx.supabase.co`)
- **anon/public key** (long JWT token starting with `eyJ...`)
## Step 3: Configure Your Application
1. Open `db.config.js` in your project
2. Replace the placeholder values:
```javascript
module.exports = {
supabaseUrl: 'https://YOUR_PROJECT_ID.supabase.co', // Paste your Project URL here
supabaseAnonKey: 'YOUR_ANON_KEY_HERE', // Paste your anon key here
supabasePassword: 't1hWsackxbYzRIPD', // Your database password
// Optional: Direct PostgreSQL connection
postgresConnectionString: 'postgresql://postgres:t1hWsackxbYzRIPD@db.YOUR_PROJECT_ID.supabase.co:5432/postgres'
};
```
## Step 4: Create Database Tables
1. In your Supabase dashboard, click on **SQL Editor** in the sidebar
2. Click **New Query**
3. Copy and paste the following SQL:
```sql
-- Create players table
CREATE TABLE IF NOT EXISTS players (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_username ON players(username);
-- Create active sessions table
CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
);
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
);
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
-- Enable Row Level Security (RLS)
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
```
4. Click **Run** or press `Ctrl+Enter`
5. You should see "Success. No rows returned" message
## Step 5: Create Helper Functions
1. In the same SQL Editor, create a new query
2. Copy and paste the contents of `supabase-functions.sql`:
```sql
-- Function to increment wins
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment losses
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment draws
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
```
3. Click **Run**
## Step 6: Test Your Connection
1. Install dependencies:
```bash
npm install
```
2. Start the server:
```bash
npm start
```
3. Check the console output for:
- ✅ Database schema verified successfully
- 🗄️ Database connected
4. Open your browser to `http://localhost:3000`
5. Check the bottom status bar - it should show:
- **SQL**: Connected (green)
- **Latency**: Should be reasonable (depends on your location to Sydney)
- **Write**: Enabled (green)
## Troubleshooting
### "Invalid API key" Error
- Double-check your `supabaseAnonKey` in `db.config.js`
- Make sure you copied the **anon/public** key, not the service_role key
### "Cannot reach Supabase" Error
- Verify your `supabaseUrl` is correct
- Check your internet connection
- Ensure no firewall is blocking Supabase
### "Table 'players' does not exist" Error
- Make sure you ran the SQL schema in Step 4
- Check the SQL Editor for any error messages
- Verify all tables were created in the **Table Editor**
### High Latency
- This is normal if you're far from the Sydney region
- Consider changing the region when creating your project
- Latency doesn't significantly affect gameplay for turn-based games
## Security Notes
- The `db.config.js` file is in `.gitignore` and will NOT be committed to Git
- Never share your database password or anon key publicly
- The anon key is safe to use in client-side code (it's protected by RLS policies)
- For production, consider implementing more restrictive RLS policies
## Next Steps
Once connected, you can:
- Test multiplayer functionality
- Monitor your database in the Supabase dashboard
- View real-time data in the **Table Editor**
- Check logs in the **Logs** section
- Set up database backups (available in paid plans)

View File

@@ -1,82 +1,110 @@
const mysql = require('mysql2/promise'); const { createClient } = require('@supabase/supabase-js');
// Import database configuration from external file // Import database configuration from external file
// This file (db.config.js) is not committed to git for security // This file (db.config.js) is not committed to git for security
// Use db.config.example.js as a template // Use db.config.example.js as a template
const dbConfig = require('./db.config.js'); const dbConfig = require('./db.config.js');
// Create connection pool // Create Supabase client
const pool = mysql.createPool(dbConfig); const supabase = createClient(dbConfig.supabaseUrl, dbConfig.supabaseAnonKey);
// Initialize database schema // Initialize database schema
async function initializeDatabase() { async function initializeDatabase() {
try { try {
const connection = await pool.getConnection(); console.log('🔄 Initializing Supabase database schema...');
// Create players table // Create players table
await connection.query(` const { error: playersError } = await supabase.rpc('create_players_table', {});
CREATE TABLE IF NOT EXISTS players (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username)
)
`);
// Create active sessions table // Since we can't run raw SQL directly with the JS client in the same way,
await connection.query(` // we'll use Supabase's SQL editor or migrations
CREATE TABLE IF NOT EXISTS active_sessions ( // For now, we'll check if tables exist by trying to query them
session_id VARCHAR(100) PRIMARY KEY,
player_id INT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
)
`);
// Create games table const { data: playersCheck, error: playersCheckError } = await supabase
await connection.query(` .from('players')
CREATE TABLE IF NOT EXISTS games ( .select('id')
id INT AUTO_INCREMENT PRIMARY KEY, .limit(1);
player1_id INT NOT NULL,
player2_id INT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id INT,
game_state ENUM('pending', 'active', 'completed', 'abandoned') DEFAULT 'pending',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
)
`);
// Create game moves table if (playersCheckError && playersCheckError.code === '42P01') {
await connection.query(` console.log('⚠️ Tables not found. Please run the following SQL in your Supabase SQL Editor:');
CREATE TABLE IF NOT EXISTS game_moves ( console.log(`
id INT AUTO_INCREMENT PRIMARY KEY, -- Create players table
game_id INT NOT NULL, CREATE TABLE IF NOT EXISTS players (
player_id INT NOT NULL, id BIGSERIAL PRIMARY KEY,
row_position INT NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
col_position INT NOT NULL, total_wins INT DEFAULT 0,
move_number INT NOT NULL, total_losses INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, total_draws INT DEFAULT 0,
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
FOREIGN KEY (player_id) REFERENCES players(id), );
INDEX idx_game (game_id) CREATE INDEX IF NOT EXISTS idx_username ON players(username);
)
`);
connection.release(); -- Create active sessions table
console.log('✅ Database schema initialized successfully'); CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
);
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
);
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
-- Enable Row Level Security (RLS) - Optional but recommended
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
`);
throw new Error('Database tables not initialized. Please run the SQL above in Supabase SQL Editor.');
}
console.log('✅ Database schema verified successfully');
} catch (error) { } catch (error) {
console.error('❌ Error initializing database:', error); console.error('❌ Error initializing database:', error.message);
throw error; throw error;
} }
} }
@@ -86,11 +114,26 @@ const db = {
// Create or get player // Create or get player
async createPlayer(username) { async createPlayer(username) {
try { try {
const [result] = await pool.query( // First try to get existing player
'INSERT INTO players (username) VALUES (?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)', const { data: existingPlayer, error: selectError } = await supabase
[username] .from('players')
); .select('id')
return result.insertId; .eq('username', username)
.single();
if (existingPlayer) {
return existingPlayer.id;
}
// If not found, create new player
const { data, error } = await supabase
.from('players')
.insert([{ username }])
.select('id')
.single();
if (error) throw error;
return data.id;
} catch (error) { } catch (error) {
console.error('Error creating player:', error); console.error('Error creating player:', error);
throw error; throw error;
@@ -99,128 +142,217 @@ const db = {
// Get player by username // Get player by username
async getPlayer(username) { async getPlayer(username) {
const [rows] = await pool.query( const { data, error } = await supabase
'SELECT * FROM players WHERE username = ?', .from('players')
[username] .select('*')
); .eq('username', username)
return rows[0]; .single();
if (error && error.code !== 'PGRST116') throw error; // PGRST116 = not found
return data;
}, },
// Get player by ID // Get player by ID
async getPlayerById(playerId) { async getPlayerById(playerId) {
const [rows] = await pool.query( const { data, error } = await supabase
'SELECT * FROM players WHERE id = ?', .from('players')
[playerId] .select('*')
); .eq('id', playerId)
return rows[0]; .single();
if (error && error.code !== 'PGRST116') throw error;
return data;
}, },
// Add active session // Add active session
async addSession(sessionId, playerId, username) { async addSession(sessionId, playerId, username) {
await pool.query( const { error } = await supabase
'INSERT INTO active_sessions (session_id, player_id, username) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE last_heartbeat = CURRENT_TIMESTAMP', .from('active_sessions')
[sessionId, playerId, username] .upsert([{
); session_id: sessionId,
player_id: playerId,
username: username,
last_heartbeat: new Date().toISOString()
}], {
onConflict: 'session_id'
});
if (error) throw error;
}, },
// Remove session // Remove session
async removeSession(sessionId) { async removeSession(sessionId) {
await pool.query( const { error } = await supabase
'DELETE FROM active_sessions WHERE session_id = ?', .from('active_sessions')
[sessionId] .delete()
); .eq('session_id', sessionId);
if (error) throw error;
}, },
// Get all active players // Get all active players
async getActivePlayers() { async getActivePlayers() {
const [rows] = await pool.query(` const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
SELECT s.session_id, s.username, p.total_wins, p.total_losses, p.total_draws
FROM active_sessions s const { data, error } = await supabase
JOIN players p ON s.player_id = p.id .from('active_sessions')
WHERE s.last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE) .select(`
`); session_id,
return rows; username,
players!inner(total_wins, total_losses, total_draws)
`)
.gt('last_heartbeat', twoMinutesAgo);
if (error) throw error;
// Flatten the response to match the old format
return data.map(row => ({
session_id: row.session_id,
username: row.username,
total_wins: row.players.total_wins,
total_losses: row.players.total_losses,
total_draws: row.players.total_draws
}));
}, },
// Create new game // Create new game
async createGame(player1Id, player2Id, player1Username, player2Username, boardSize) { async createGame(player1Id, player2Id, player1Username, player2Username, boardSize) {
const [result] = await pool.query( const { data, error } = await supabase
'INSERT INTO games (player1_id, player2_id, player1_username, player2_username, board_size, game_state) VALUES (?, ?, ?, ?, ?, ?)', .from('games')
[player1Id, player2Id, player1Username, player2Username, boardSize, 'active'] .insert([{
); player1_id: player1Id,
return result.insertId; player2_id: player2Id,
player1_username: player1Username,
player2_username: player2Username,
board_size: boardSize,
game_state: 'active'
}])
.select('id')
.single();
if (error) throw error;
return data.id;
}, },
// Record move // Record move
async recordMove(gameId, playerId, row, col, moveNumber) { async recordMove(gameId, playerId, row, col, moveNumber) {
await pool.query( const { error } = await supabase
'INSERT INTO game_moves (game_id, player_id, row_position, col_position, move_number) VALUES (?, ?, ?, ?, ?)', .from('game_moves')
[gameId, playerId, row, col, moveNumber] .insert([{
); game_id: gameId,
player_id: playerId,
row_position: row,
col_position: col,
move_number: moveNumber
}]);
if (error) throw error;
}, },
// Complete game // Complete game
async completeGame(gameId, winnerId) { async completeGame(gameId, winnerId) {
await pool.query( // Update game status
'UPDATE games SET game_state = ?, winner_id = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?', const { error: gameError } = await supabase
['completed', winnerId, gameId] .from('games')
); .update({
game_state: 'completed',
winner_id: winnerId,
completed_at: new Date().toISOString()
})
.eq('id', gameId);
if (gameError) throw gameError;
// Update player stats // Update player stats
if (winnerId) { if (winnerId) {
// Get game details // Get game details
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]); const { data: game, error: selectError } = await supabase
if (game.length > 0) { .from('games')
const loserId = game[0].player1_id === winnerId ? game[0].player2_id : game[0].player1_id; .select('player1_id, player2_id')
.eq('id', gameId)
.single();
if (selectError) throw selectError;
if (game) {
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
// Update winner // Update winner
await pool.query('UPDATE players SET total_wins = total_wins + 1 WHERE id = ?', [winnerId]); await supabase.rpc('increment_wins', { player_id: winnerId });
// Update loser // Update loser
await pool.query('UPDATE players SET total_losses = total_losses + 1 WHERE id = ?', [loserId]); await supabase.rpc('increment_losses', { player_id: loserId });
} }
} else { } else {
// Draw - update both players // Draw - update both players
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]); const { data: game, error: selectError } = await supabase
if (game.length > 0) { .from('games')
await pool.query('UPDATE players SET total_draws = total_draws + 1 WHERE id IN (?, ?)', .select('player1_id, player2_id')
[game[0].player1_id, game[0].player2_id]); .eq('id', gameId)
.single();
if (selectError) throw selectError;
if (game) {
await supabase.rpc('increment_draws', { player_id: game.player1_id });
await supabase.rpc('increment_draws', { player_id: game.player2_id });
} }
} }
}, },
// Abandon game // Abandon game
async abandonGame(gameId, winnerId) { async abandonGame(gameId, winnerId) {
await pool.query( // Update game status
'UPDATE games SET game_state = ?, winner_id = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?', const { error: gameError } = await supabase
['abandoned', winnerId, gameId] .from('games')
); .update({
game_state: 'abandoned',
winner_id: winnerId,
completed_at: new Date().toISOString()
})
.eq('id', gameId);
if (gameError) throw gameError;
// Update stats (winner gets win, other player gets loss) // Update stats (winner gets win, other player gets loss)
if (winnerId) { if (winnerId) {
const [game] = await pool.query('SELECT player1_id, player2_id FROM games WHERE id = ?', [gameId]); const { data: game, error: selectError } = await supabase
if (game.length > 0) { .from('games')
const loserId = game[0].player1_id === winnerId ? game[0].player2_id : game[0].player1_id; .select('player1_id, player2_id')
await pool.query('UPDATE players SET total_wins = total_wins + 1 WHERE id = ?', [winnerId]); .eq('id', gameId)
await pool.query('UPDATE players SET total_losses = total_losses + 1 WHERE id = ?', [loserId]); .single();
if (selectError) throw selectError;
if (game) {
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
await supabase.rpc('increment_wins', { player_id: winnerId });
await supabase.rpc('increment_losses', { player_id: loserId });
} }
} }
}, },
// Update heartbeat // Update heartbeat
async updateHeartbeat(sessionId) { async updateHeartbeat(sessionId) {
await pool.query( const { error } = await supabase
'UPDATE active_sessions SET last_heartbeat = CURRENT_TIMESTAMP WHERE session_id = ?', .from('active_sessions')
[sessionId] .update({ last_heartbeat: new Date().toISOString() })
); .eq('session_id', sessionId);
if (error) throw error;
}, },
// Clean up stale sessions // Clean up stale sessions
async cleanupStaleSessions() { async cleanupStaleSessions() {
await pool.query( const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
'DELETE FROM active_sessions WHERE last_heartbeat < DATE_SUB(NOW(), INTERVAL 2 MINUTE)'
); const { error } = await supabase
.from('active_sessions')
.delete()
.lt('last_heartbeat', twoMinutesAgo);
if (error) throw error;
} }
}; };
module.exports = { pool, initializeDatabase, db }; module.exports = { supabase, initializeDatabase, db };

View File

@@ -1,13 +1,18 @@
// Database Configuration File // Database Configuration File - EXAMPLE
// IMPORTANT: This file contains sensitive credentials and should NEVER be committed to git // Copy this file to db.config.js and fill in your actual Supabase credentials
// Copy this file to db.config.js and update with your actual database credentials // DO NOT commit db.config.js to git - it's in .gitignore
// Supabase Configuration
// Get these values from your Supabase project dashboard:
// 1. Go to https://app.supabase.com
// 2. Select your project
// 3. Go to Project Settings → API
module.exports = { module.exports = {
host: 'your-database-host.com', supabaseUrl: 'https://xxxxxxxxxxxxx.supabase.co', // Your Supabase project URL
user: 'your-database-username', supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', // Your Supabase anon/public key
password: 'your-secure-password', supabasePassword: 'your_database_password_here', // Your database password
database: 'your-database-name',
waitForConnections: true, // Optional: Direct PostgreSQL connection string
connectionLimit: 10, // Found in Project Settings → Database → Connection String
queueLimit: 0 postgresConnectionString: 'postgresql://postgres:your_password@db.xxxxxxxxxxxxx.supabase.co:5432/postgres'
}; };

132
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.39.0",
"bad-words": "^3.0.4", "bad-words": "^3.0.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
@@ -25,6 +26,107 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@supabase/auth-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz",
"integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz",
"integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz",
"integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz",
"integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@supabase/storage-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz",
"integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz",
"integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.89.0",
"@supabase/functions-js": "2.89.0",
"@supabase/postgrest-js": "2.89.0",
"@supabase/realtime-js": "2.89.0",
"@supabase/storage-js": "2.89.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@types/cors": { "node_modules/@types/cors": {
"version": "2.8.19", "version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -43,6 +145,21 @@
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -706,6 +823,15 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1597,6 +1723,12 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@@ -20,6 +20,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"socket.io": "^4.6.1", "socket.io": "^4.6.1",
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"@supabase/supabase-js": "^2.39.0",
"bad-words": "^3.0.4", "bad-words": "^3.0.4",
"cors": "^2.8.5" "cors": "^2.8.5"
}, },

View File

@@ -3,7 +3,7 @@ const http = require('http');
const socketIO = require('socket.io'); const socketIO = require('socket.io');
const cors = require('cors'); const cors = require('cors');
const path = require('path'); const path = require('path');
const { initializeDatabase, db } = require('./database'); const { initializeDatabase, db, supabase } = require('./database');
const GameManager = require('./gameManager'); const GameManager = require('./gameManager');
const app = express(); const app = express();
@@ -39,96 +39,74 @@ app.get('/api/db-status', async (req, res) => {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: null, error: null,
// Additional diagnostic info for testing phase // Additional diagnostic info for testing phase
host: dbConfig.host || 'unknown', supabaseUrl: dbConfig.supabaseUrl || 'unknown',
database: dbConfig.database || 'unknown', database: 'Supabase PostgreSQL',
user: dbConfig.user || 'unknown', connectionType: 'Supabase Client'
connectionLimit: dbConfig.connectionLimit || 'unknown',
poolStats: null
}; };
try { try {
console.log(`[DB-STATUS] Testing connection to ${status.host}/${status.database}...`); console.log(`[DB-STATUS] Testing Supabase connection to ${status.supabaseUrl}...`);
// Test connection with a simple query // Test connection with a simple query
const [result] = await db.pool.query('SELECT 1 as test'); const { data, error } = await supabase
.from('players')
.select('id')
.limit(1);
const latency = Date.now() - startTime; const latency = Date.now() - startTime;
if (result && result[0].test === 1) { if (!error || error.code === 'PGRST116') { // PGRST116 = no rows found (table exists but empty)
status.connected = true; status.connected = true;
status.latency = latency; status.latency = latency;
console.log(`[DB-STATUS] ✅ Connection successful (${latency}ms)`); console.log(`[DB-STATUS] ✅ Connection successful (${latency}ms)`);
// Get pool statistics
try {
status.poolStats = {
totalConnections: db.pool.pool._allConnections.length,
freeConnections: db.pool.pool._freeConnections.length,
queuedRequests: db.pool.pool._connectionQueue.length
};
console.log(`[DB-STATUS] Pool stats:`, status.poolStats);
} catch (poolError) {
console.log(`[DB-STATUS] Could not retrieve pool stats:`, poolError.message);
}
// Test write capability // Test write capability
try { try {
console.log(`[DB-STATUS] Testing write capability...`); console.log(`[DB-STATUS] Testing write capability...`);
const testTableName = '_health_check_test';
// Create test table if it doesn't exist
await db.pool.query(`
CREATE TABLE IF NOT EXISTS ${testTableName} (
id INT AUTO_INCREMENT PRIMARY KEY,
test_value VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Try to insert a test record // Try to insert a test record
const testValue = `test_${Date.now()}`; const testUsername = `_test_${Date.now()}`;
await db.pool.query( const { data: insertData, error: insertError } = await supabase
`INSERT INTO ${testTableName} (test_value) VALUES (?)`, .from('players')
[testValue] .insert([{ username: testUsername }])
); .select('id')
.single();
// Clean up old test records (keep only last 10) if (!insertError && insertData) {
await db.pool.query(` // Clean up test record
DELETE FROM ${testTableName} await supabase
WHERE id NOT IN ( .from('players')
SELECT id FROM ( .delete()
SELECT id FROM ${testTableName} .eq('id', insertData.id);
ORDER BY created_at DESC
LIMIT 10
) AS keep_records
)
`);
status.writeCapable = true; status.writeCapable = true;
console.log(`[DB-STATUS] ✅ Write test successful`); console.log(`[DB-STATUS] ✅ Write test successful`);
} else {
throw insertError;
}
} catch (writeError) { } catch (writeError) {
console.error(`[DB-STATUS] ❌ Write test failed:`, writeError.message); console.error(`[DB-STATUS] ❌ Write test failed:`, writeError.message);
status.writeCapable = false; status.writeCapable = false;
status.error = `Write test failed: ${writeError.message}`; status.error = `Write test failed: ${writeError.message}`;
} }
} else {
throw error;
} }
} catch (error) { } catch (error) {
console.error(`[DB-STATUS] ❌ Connection failed:`, error.message); console.error(`[DB-STATUS] ❌ Connection failed:`, error.message);
console.error(`[DB-STATUS] Error code:`, error.code); console.error(`[DB-STATUS] Error details:`, error);
console.error(`[DB-STATUS] Error errno:`, error.errno);
status.connected = false; status.connected = false;
status.latency = Date.now() - startTime; status.latency = Date.now() - startTime;
// Provide more detailed error messages // Provide more detailed error messages
let errorMessage = error.message; let errorMessage = error.message;
if (error.code === 'ECONNREFUSED') { if (error.code === '42P01') {
errorMessage = `Connection refused to ${status.host}. Is MySQL running?`; errorMessage = `Table 'players' does not exist. Please run the SQL schema in Supabase SQL Editor.`;
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') { } else if (error.message && error.message.includes('Invalid API key')) {
errorMessage = `Access denied for user '${status.user}'. Check credentials.`; errorMessage = `Invalid Supabase API key. Check your db.config.js file.`;
} else if (error.code === 'ER_BAD_DB_ERROR') { } else if (error.message && error.message.includes('fetch')) {
errorMessage = `Database '${status.database}' does not exist.`; errorMessage = `Cannot reach Supabase. Check your supabaseUrl in db.config.js.`;
} else if (error.code === 'ENOTFOUND') {
errorMessage = `Host '${status.host}' not found. Check hostname.`;
} }
status.error = errorMessage; status.error = errorMessage;

50
supabase-functions.sql Normal file
View File

@@ -0,0 +1,50 @@
-- Supabase SQL Helper Functions for Connect-5
-- Run this in your Supabase SQL Editor after creating the tables
-- Function to increment wins
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment losses
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Function to increment draws
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Optional: Function to get player stats
CREATE OR REPLACE FUNCTION get_player_stats(player_username VARCHAR)
RETURNS TABLE (
id BIGINT,
username VARCHAR,
total_wins INT,
total_losses INT,
total_draws INT,
created_at TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.username, p.total_wins, p.total_losses, p.total_draws, p.created_at
FROM players p
WHERE p.username = player_username;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,114 @@
-- Complete Supabase Schema for Connect-5
-- Copy and paste this entire file into Supabase SQL Editor and run it
-- Create players table
CREATE TABLE IF NOT EXISTS players (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
total_wins INT DEFAULT 0,
total_losses INT DEFAULT 0,
total_draws INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_username ON players(username);
-- Create active sessions table
CREATE TABLE IF NOT EXISTS active_sessions (
session_id VARCHAR(100) PRIMARY KEY,
player_id BIGINT NOT NULL,
username VARCHAR(50) NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
);
-- Create game state enum type
DO $$ BEGIN
CREATE TYPE game_state_enum AS ENUM ('pending', 'active', 'completed', 'abandoned');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create games table
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player1_id BIGINT NOT NULL,
player2_id BIGINT NOT NULL,
player1_username VARCHAR(50) NOT NULL,
player2_username VARCHAR(50) NOT NULL,
board_size INT DEFAULT 15,
winner_id BIGINT,
game_state game_state_enum DEFAULT 'pending',
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (player1_id) REFERENCES players(id),
FOREIGN KEY (player2_id) REFERENCES players(id),
FOREIGN KEY (winner_id) REFERENCES players(id)
);
-- Create game moves table
CREATE TABLE IF NOT EXISTS game_moves (
id BIGSERIAL PRIMARY KEY,
game_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
row_position INT NOT NULL,
col_position INT NOT NULL,
move_number INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX IF NOT EXISTS idx_game ON game_moves(game_id);
-- Enable Row Level Security (RLS)
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
ALTER TABLE game_moves ENABLE ROW LEVEL SECURITY;
-- Create policies to allow all operations (adjust based on your security needs)
DROP POLICY IF EXISTS "Allow all operations on players" ON players;
CREATE POLICY "Allow all operations on players" ON players FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on active_sessions" ON active_sessions;
CREATE POLICY "Allow all operations on active_sessions" ON active_sessions FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on games" ON games;
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
DROP POLICY IF EXISTS "Allow all operations on game_moves" ON game_moves;
CREATE POLICY "Allow all operations on game_moves" ON game_moves FOR ALL USING (true);
-- Helper Functions
CREATE OR REPLACE FUNCTION increment_wins(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_wins = total_wins + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION increment_losses(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_losses = total_losses + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION increment_draws(player_id BIGINT)
RETURNS void AS $$
BEGIN
UPDATE players
SET total_draws = total_draws + 1
WHERE id = player_id;
END;
$$ LANGUAGE plpgsql;
-- Success message
DO $$
BEGIN
RAISE NOTICE '✅ Connect-5 database schema created successfully!';
END $$;