mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 22:46:00 +00:00
- 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
359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
const { createClient } = require('@supabase/supabase-js');
|
|
|
|
// 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);
|
|
|
|
// Initialize database schema
|
|
async function initializeDatabase() {
|
|
try {
|
|
console.log('🔄 Initializing Supabase database schema...');
|
|
|
|
// Create players table
|
|
const { error: playersError } = await supabase.rpc('create_players_table', {});
|
|
|
|
// 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:');
|
|
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);
|
|
`);
|
|
throw new Error('Database tables not initialized. Please run the SQL above in Supabase SQL Editor.');
|
|
}
|
|
|
|
console.log('✅ Database schema verified successfully');
|
|
} catch (error) {
|
|
console.error('❌ Error initializing database:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Database query functions
|
|
const db = {
|
|
// Create or get player
|
|
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();
|
|
|
|
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) {
|
|
console.error('Error creating player:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Get player by username
|
|
async getPlayer(username) {
|
|
const { data, error } = await supabase
|
|
.from('players')
|
|
.select('*')
|
|
.eq('username', username)
|
|
.single();
|
|
|
|
if (error && error.code !== 'PGRST116') throw error; // PGRST116 = not found
|
|
return data;
|
|
},
|
|
|
|
// Get player by ID
|
|
async getPlayerById(playerId) {
|
|
const { data, error } = await supabase
|
|
.from('players')
|
|
.select('*')
|
|
.eq('id', playerId)
|
|
.single();
|
|
|
|
if (error && error.code !== 'PGRST116') throw error;
|
|
return data;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// Remove session
|
|
async removeSession(sessionId) {
|
|
const { error } = await supabase
|
|
.from('active_sessions')
|
|
.delete()
|
|
.eq('session_id', sessionId);
|
|
|
|
if (error) throw error;
|
|
},
|
|
|
|
// Get all active players
|
|
async getActivePlayers() {
|
|
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
|
|
|
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
|
|
}));
|
|
},
|
|
|
|
// 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();
|
|
|
|
if (error) throw error;
|
|
return data.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;
|
|
},
|
|
|
|
// 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;
|
|
|
|
// 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();
|
|
|
|
if (selectError) throw selectError;
|
|
|
|
if (game) {
|
|
const loserId = game.player1_id === winnerId ? game.player2_id : game.player1_id;
|
|
|
|
// Update winner
|
|
await supabase.rpc('increment_wins', { player_id: winnerId });
|
|
|
|
// Update loser
|
|
await supabase.rpc('increment_losses', { player_id: loserId });
|
|
}
|
|
} else {
|
|
// Draw - update both players
|
|
const { data: game, error: selectError } = await supabase
|
|
.from('games')
|
|
.select('player1_id, 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
|
|
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;
|
|
|
|
// 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();
|
|
|
|
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
|
|
async updateHeartbeat(sessionId) {
|
|
const { error } = await supabase
|
|
.from('active_sessions')
|
|
.update({ last_heartbeat: new Date().toISOString() })
|
|
.eq('session_id', sessionId);
|
|
|
|
if (error) throw error;
|
|
},
|
|
|
|
// 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;
|
|
}
|
|
};
|
|
|
|
module.exports = { supabase, initializeDatabase, db };
|