mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 20:36:00 +00:00
Migrate from Supabase to direct PostgreSQL connection
- Replace @supabase/supabase-js with native pg library - Rewrite database.js to use PostgreSQL connection pool - Update server.js with PostgreSQL connection testing - Create postgres-schema.sql with complete database schema - Add apply-schema.js script for easy schema deployment - Update all documentation (README.md, DEPLOYMENT.md, deploy.sh) - Remove Supabase-specific files and references - Update db.config.example.js with PostgreSQL format
This commit is contained in:
377
database.js
377
database.js
@@ -1,105 +1,49 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Import database configuration from external file
|
||||
// This file (db.config.js) is not committed to git for security
|
||||
// Use db.config.example.js as a template
|
||||
const dbConfig = require('./db.config.js');
|
||||
|
||||
// Create Supabase client
|
||||
const supabase = createClient(dbConfig.supabaseUrl, dbConfig.supabaseAnonKey);
|
||||
// Create PostgreSQL connection pool
|
||||
const pool = new Pool({
|
||||
host: dbConfig.HOST,
|
||||
user: dbConfig.USER,
|
||||
password: dbConfig.PASSWORD,
|
||||
database: dbConfig.DB,
|
||||
port: 5432,
|
||||
max: dbConfig.pool.max,
|
||||
min: dbConfig.pool.min,
|
||||
acquireTimeoutMillis: dbConfig.pool.acquire,
|
||||
idleTimeoutMillis: dbConfig.pool.idle
|
||||
});
|
||||
|
||||
// Handle pool errors
|
||||
pool.on('error', (err) => {
|
||||
console.error('Unexpected error on idle PostgreSQL client', err);
|
||||
});
|
||||
|
||||
// Initialize database schema
|
||||
async function initializeDatabase() {
|
||||
try {
|
||||
console.log('🔄 Initializing Supabase database schema...');
|
||||
console.log('🔄 Initializing PostgreSQL database schema...');
|
||||
|
||||
// Create players table
|
||||
const { error: playersError } = await supabase.rpc('create_players_table', {});
|
||||
// Check if tables exist by trying to query them
|
||||
const result = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'players'
|
||||
);
|
||||
`);
|
||||
|
||||
// Since we can't run raw SQL directly with the JS client in the same way,
|
||||
// we'll use Supabase's SQL editor or migrations
|
||||
// For now, we'll check if tables exist by trying to query them
|
||||
|
||||
const { data: playersCheck, error: playersCheckError } = await supabase
|
||||
.from('players')
|
||||
.select('id')
|
||||
.limit(1);
|
||||
|
||||
if (playersCheckError && playersCheckError.code === '42P01') {
|
||||
console.log('⚠️ Tables not found. Please run the following SQL in your Supabase SQL Editor:');
|
||||
if (!result.rows[0].exists) {
|
||||
console.log('⚠️ Tables not found. Please run the following SQL in your PostgreSQL database:');
|
||||
console.log(`
|
||||
-- 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) - 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);
|
||||
Run the postgres-schema.sql file in your PostgreSQL database:
|
||||
psql -h ${dbConfig.HOST} -U ${dbConfig.USER} -d ${dbConfig.DB} -f postgres-schema.sql
|
||||
`);
|
||||
throw new Error('Database tables not initialized. Please run the SQL above in Supabase SQL Editor.');
|
||||
throw new Error('Database tables not initialized. Please run postgres-schema.sql first.');
|
||||
}
|
||||
|
||||
console.log('✅ Database schema verified successfully');
|
||||
@@ -115,25 +59,22 @@ const db = {
|
||||
async createPlayer(username) {
|
||||
try {
|
||||
// First try to get existing player
|
||||
const { data: existingPlayer, error: selectError } = await supabase
|
||||
.from('players')
|
||||
.select('id')
|
||||
.eq('username', username)
|
||||
.single();
|
||||
const selectResult = await pool.query(
|
||||
'SELECT id FROM players WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (existingPlayer) {
|
||||
return existingPlayer.id;
|
||||
if (selectResult.rows.length > 0) {
|
||||
return selectResult.rows[0].id;
|
||||
}
|
||||
|
||||
// If not found, create new player
|
||||
const { data, error } = await supabase
|
||||
.from('players')
|
||||
.insert([{ username }])
|
||||
.select('id')
|
||||
.single();
|
||||
const insertResult = await pool.query(
|
||||
'INSERT INTO players (username) VALUES ($1) RETURNING id',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
return data.id;
|
||||
return insertResult.rows[0].id;
|
||||
} catch (error) {
|
||||
console.error('Error creating player:', error);
|
||||
throw error;
|
||||
@@ -142,160 +83,120 @@ const db = {
|
||||
|
||||
// Get player by username
|
||||
async getPlayer(username) {
|
||||
const { data, error } = await supabase
|
||||
.from('players')
|
||||
.select('*')
|
||||
.eq('username', username)
|
||||
.single();
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM players WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error; // PGRST116 = not found
|
||||
return data;
|
||||
return result.rows[0] || null;
|
||||
},
|
||||
|
||||
// Get player by ID
|
||||
async getPlayerById(playerId) {
|
||||
const { data, error } = await supabase
|
||||
.from('players')
|
||||
.select('*')
|
||||
.eq('id', playerId)
|
||||
.single();
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM players WHERE id = $1',
|
||||
[playerId]
|
||||
);
|
||||
|
||||
if (error && error.code !== 'PGRST116') throw error;
|
||||
return data;
|
||||
return result.rows[0] || null;
|
||||
},
|
||||
|
||||
// Add active session
|
||||
async addSession(sessionId, playerId, username) {
|
||||
const { error } = await supabase
|
||||
.from('active_sessions')
|
||||
.upsert([{
|
||||
session_id: sessionId,
|
||||
player_id: playerId,
|
||||
username: username,
|
||||
last_heartbeat: new Date().toISOString()
|
||||
}], {
|
||||
onConflict: 'session_id'
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
await pool.query(
|
||||
`INSERT INTO active_sessions (session_id, player_id, username, last_heartbeat)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
ON CONFLICT (session_id)
|
||||
DO UPDATE SET last_heartbeat = NOW()`,
|
||||
[sessionId, playerId, username]
|
||||
);
|
||||
},
|
||||
|
||||
// Remove session
|
||||
async removeSession(sessionId) {
|
||||
const { error } = await supabase
|
||||
.from('active_sessions')
|
||||
.delete()
|
||||
.eq('session_id', sessionId);
|
||||
|
||||
if (error) throw error;
|
||||
await pool.query(
|
||||
'DELETE FROM active_sessions WHERE session_id = $1',
|
||||
[sessionId]
|
||||
);
|
||||
},
|
||||
|
||||
// Get all active players
|
||||
async getActivePlayers() {
|
||||
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
s.session_id,
|
||||
s.username,
|
||||
p.total_wins,
|
||||
p.total_losses,
|
||||
p.total_draws
|
||||
FROM active_sessions s
|
||||
INNER JOIN players p ON s.player_id = p.id
|
||||
WHERE s.last_heartbeat > NOW() - INTERVAL '2 minutes'`
|
||||
);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('active_sessions')
|
||||
.select(`
|
||||
session_id,
|
||||
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
|
||||
}));
|
||||
return result.rows;
|
||||
},
|
||||
|
||||
// Create new game
|
||||
async createGame(player1Id, player2Id, player1Username, player2Username, boardSize) {
|
||||
const { data, error } = await supabase
|
||||
.from('games')
|
||||
.insert([{
|
||||
player1_id: player1Id,
|
||||
player2_id: player2Id,
|
||||
player1_username: player1Username,
|
||||
player2_username: player2Username,
|
||||
board_size: boardSize,
|
||||
game_state: 'active'
|
||||
}])
|
||||
.select('id')
|
||||
.single();
|
||||
const result = await pool.query(
|
||||
`INSERT INTO games (player1_id, player2_id, player1_username, player2_username, board_size, game_state)
|
||||
VALUES ($1, $2, $3, $4, $5, 'active')
|
||||
RETURNING id`,
|
||||
[player1Id, player2Id, player1Username, player2Username, boardSize]
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
return data.id;
|
||||
return result.rows[0].id;
|
||||
},
|
||||
|
||||
// Record move
|
||||
async recordMove(gameId, playerId, row, col, moveNumber) {
|
||||
const { error } = await supabase
|
||||
.from('game_moves')
|
||||
.insert([{
|
||||
game_id: gameId,
|
||||
player_id: playerId,
|
||||
row_position: row,
|
||||
col_position: col,
|
||||
move_number: moveNumber
|
||||
}]);
|
||||
|
||||
if (error) throw error;
|
||||
await pool.query(
|
||||
`INSERT INTO game_moves (game_id, player_id, row_position, col_position, move_number)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[gameId, playerId, row, col, moveNumber]
|
||||
);
|
||||
},
|
||||
|
||||
// Complete game
|
||||
async completeGame(gameId, winnerId) {
|
||||
// Update game status
|
||||
const { error: gameError } = await supabase
|
||||
.from('games')
|
||||
.update({
|
||||
game_state: 'completed',
|
||||
winner_id: winnerId,
|
||||
completed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', gameId);
|
||||
|
||||
if (gameError) throw gameError;
|
||||
await pool.query(
|
||||
`UPDATE games
|
||||
SET game_state = 'completed', winner_id = $1, completed_at = NOW()
|
||||
WHERE id = $2`,
|
||||
[winnerId, gameId]
|
||||
);
|
||||
|
||||
// Update player stats
|
||||
if (winnerId) {
|
||||
// Get game details
|
||||
const { data: game, error: selectError } = await supabase
|
||||
.from('games')
|
||||
.select('player1_id, player2_id')
|
||||
.eq('id', gameId)
|
||||
.single();
|
||||
const gameResult = await pool.query(
|
||||
'SELECT player1_id, player2_id FROM games WHERE id = $1',
|
||||
[gameId]
|
||||
);
|
||||
|
||||
if (selectError) throw selectError;
|
||||
|
||||
if (game) {
|
||||
if (gameResult.rows.length > 0) {
|
||||
const game = gameResult.rows[0];
|
||||
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
|
||||
|
||||
// Update winner
|
||||
await supabase.rpc('increment_wins', { player_id: winnerId });
|
||||
await pool.query('SELECT increment_wins($1)', [winnerId]);
|
||||
|
||||
// Update loser
|
||||
await supabase.rpc('increment_losses', { player_id: loserId });
|
||||
await pool.query('SELECT increment_losses($1)', [loserId]);
|
||||
}
|
||||
} else {
|
||||
// Draw - update both players
|
||||
const { data: game, error: selectError } = await supabase
|
||||
.from('games')
|
||||
.select('player1_id, player2_id')
|
||||
.eq('id', gameId)
|
||||
.single();
|
||||
const gameResult = await pool.query(
|
||||
'SELECT player1_id, player2_id FROM games WHERE id = $1',
|
||||
[gameId]
|
||||
);
|
||||
|
||||
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 });
|
||||
if (gameResult.rows.length > 0) {
|
||||
const game = gameResult.rows[0];
|
||||
await pool.query('SELECT increment_draws($1)', [game.player1_id]);
|
||||
await pool.query('SELECT increment_draws($1)', [game.player2_id]);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -303,56 +204,44 @@ const db = {
|
||||
// Abandon game
|
||||
async abandonGame(gameId, winnerId) {
|
||||
// Update game status
|
||||
const { error: gameError } = await supabase
|
||||
.from('games')
|
||||
.update({
|
||||
game_state: 'abandoned',
|
||||
winner_id: winnerId,
|
||||
completed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', gameId);
|
||||
|
||||
if (gameError) throw gameError;
|
||||
await pool.query(
|
||||
`UPDATE games
|
||||
SET game_state = 'abandoned', winner_id = $1, completed_at = NOW()
|
||||
WHERE id = $2`,
|
||||
[winnerId, gameId]
|
||||
);
|
||||
|
||||
// Update stats (winner gets win, other player gets loss)
|
||||
if (winnerId) {
|
||||
const { data: game, error: selectError } = await supabase
|
||||
.from('games')
|
||||
.select('player1_id, player2_id')
|
||||
.eq('id', gameId)
|
||||
.single();
|
||||
const gameResult = await pool.query(
|
||||
'SELECT player1_id, player2_id FROM games WHERE id = $1',
|
||||
[gameId]
|
||||
);
|
||||
|
||||
if (selectError) throw selectError;
|
||||
|
||||
if (game) {
|
||||
if (gameResult.rows.length > 0) {
|
||||
const game = gameResult.rows[0];
|
||||
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 });
|
||||
await pool.query('SELECT increment_wins($1)', [winnerId]);
|
||||
await pool.query('SELECT increment_losses($1)', [loserId]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update heartbeat
|
||||
async updateHeartbeat(sessionId) {
|
||||
const { error } = await supabase
|
||||
.from('active_sessions')
|
||||
.update({ last_heartbeat: new Date().toISOString() })
|
||||
.eq('session_id', sessionId);
|
||||
|
||||
if (error) throw error;
|
||||
await pool.query(
|
||||
'UPDATE active_sessions SET last_heartbeat = NOW() WHERE session_id = $1',
|
||||
[sessionId]
|
||||
);
|
||||
},
|
||||
|
||||
// Clean up stale sessions
|
||||
async cleanupStaleSessions() {
|
||||
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('active_sessions')
|
||||
.delete()
|
||||
.lt('last_heartbeat', twoMinutesAgo);
|
||||
|
||||
if (error) throw error;
|
||||
await pool.query(
|
||||
`DELETE FROM active_sessions
|
||||
WHERE last_heartbeat < NOW() - INTERVAL '2 minutes'`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { supabase, initializeDatabase, db };
|
||||
module.exports = { pool, initializeDatabase, db };
|
||||
|
||||
Reference in New Issue
Block a user