mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-20 07:06:00 +00:00
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:
@@ -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
223
SUPABASE_SETUP.md
Normal 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)
|
||||||
394
database.js
394
database.js
@@ -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 };
|
||||||
|
|||||||
@@ -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
132
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
96
server.js
96
server.js
@@ -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
50
supabase-functions.sql
Normal 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;
|
||||||
114
supabase-schema-complete.sql
Normal file
114
supabase-schema-complete.sql
Normal 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 $$;
|
||||||
Reference in New Issue
Block a user