Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions backend/src/controllers/geminiController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Request, Response } from "express";
import { runPrompt } from "../services/geminiService.js";

// Demo to test prompts for now
// Once tournament generation is complete, this will be integrated
export const runGeminiDemo = async (req: Request, res: Response) => {
const { prompt } = req.body ?? {};

if (typeof prompt !== "string" || prompt.trim().length === 0) {
return res.status(400).json({ error: "Prompt is required" });
}

try {
const result = await runPrompt(prompt);
return res.json(result);
} catch (err) {
console.error("GEMINI DEMO ERROR:", err);

const message =
err instanceof Error
? err.message
: "Gemini request failed. Verify GEMINI_API_KEY and GEMINI_MODEL.";

return res.status(500).json({
error: message,
hint:
"Ensure GEMINI_API_KEY is valid and GEMINI_MODEL (default gemini-2.5-flash) is supported. Try a smaller Flash/Pro variant if needed.",
});
}
};
216 changes: 216 additions & 0 deletions backend/src/controllers/tournamentController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import type { Request, Response } from "express";
import {
createTournament,
generateTournamentSchedule,
getTournamentBracket,
getTournamentSchedule,
updateTournamentMatchResult,
type CreateTournamentPayload,
} from "../services/tournamentService.js";

const parseTournamentId = (idParam: string): number | null => {
const parsed = Number(idParam);
if (!Number.isInteger(parsed) || parsed <= 0) {
return null;
}

return parsed;
};

export const createTournamentHandler = async (req: Request, res: Response) => {
try {
const payload = req.body as Partial<CreateTournamentPayload>;

if (
typeof payload.name !== "string" ||
typeof payload.sport !== "string" ||
(payload.bracketType !== "single_elimination" && payload.bracketType !== "round_robin") ||
typeof payload.startDate !== "string" ||
typeof payload.endDate !== "string" ||
typeof payload.createdBy !== "number" ||
!Array.isArray(payload.teams) ||
!Array.isArray(payload.venues)
) {
return res.status(400).json({
success: false,
message: "Invalid request body.",
});
}

const result = await createTournament({
name: payload.name,
sport: payload.sport,
bracketType: payload.bracketType,
startDate: payload.startDate,
endDate: payload.endDate,
createdBy: payload.createdBy,
teams: payload.teams,
venues: payload.venues,
});

return res.status(201).json({
success: true,
tournamentId: result.tournamentId,
});
} catch (error) {
// Log the underlying error for diagnostics, but do not expose details to the client.
console.error("Failed to create tournament:", error);
return res.status(500).json({
success: false,
message: "Server error",
});
}
Comment thread
rapidcoderx marked this conversation as resolved.
Comment thread
rapidcoderx marked this conversation as resolved.
};

export const generateScheduleHandler = async (req: Request, res: Response) => {
const idParam = req.params.id;

if (typeof idParam !== "string") {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

const tournamentId = parseTournamentId(idParam);

if (!tournamentId) {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

try {
const result = await generateTournamentSchedule(tournamentId);
return res.status(200).json({
success: true,
generatedMatches: result.generatedMatches,
});
} catch (error) {
if (error instanceof Error && error.message === "Tournament not found.") {
return res.status(404).json({
success: false,
message: error.message,
});
}

// Log unexpected errors and return a generic server error response.
console.error("Failed to generate tournament schedule:", error);
return res.status(500).json({
success: false,
message: "Server error",
});
}
Comment thread
rapidcoderx marked this conversation as resolved.
Comment thread
rapidcoderx marked this conversation as resolved.
};

export const getScheduleHandler = async (req: Request, res: Response) => {
const idParam = req.params.id;

if (typeof idParam !== "string") {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

const tournamentId = parseTournamentId(idParam);

if (!tournamentId) {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

try {
const schedule = await getTournamentSchedule(tournamentId);

return res.status(200).json({
success: true,
schedule,
});
} catch (error) {
console.error("Failed to get tournament schedule:", error);
return res.status(500).json({
success: false,
message: "Server error",
});
}
};

export const getBracketHandler = async (req: Request, res: Response) => {
const idParam = req.params.id;

if (typeof idParam !== "string") {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

const tournamentId = parseTournamentId(idParam);

if (!tournamentId) {
return res.status(400).json({
success: false,
message: "Invalid tournament id.",
});
}

try {
const bracket = await getTournamentBracket(tournamentId);
return res.status(200).json({ success: true, bracket });
} catch (error) {
if (error instanceof Error && error.message === "Tournament not found.") {
return res.status(404).json({ success: false, message: error.message });
}

console.error("Failed to get tournament bracket:", error);
return res.status(500).json({ success: false, message: "Server error" });
}
};

export const updateMatchResultHandler = async (req: Request, res: Response) => {
const tournamentIdParam = req.params.id;
const matchIdParam = req.params.matchId;

if (typeof tournamentIdParam !== "string" || typeof matchIdParam !== "string") {
return res.status(400).json({ success: false, message: "Invalid route parameters." });
}

const tournamentId = parseTournamentId(tournamentIdParam);
const matchId = parseTournamentId(matchIdParam);

if (!tournamentId || !matchId) {
return res.status(400).json({ success: false, message: "Invalid route parameters." });
}

const { homeScore, awayScore } = req.body as { homeScore?: number; awayScore?: number };

if (typeof homeScore !== "number" || typeof awayScore !== "number") {
return res.status(400).json({ success: false, message: "homeScore and awayScore are required." });
}

try {
const result = await updateTournamentMatchResult(tournamentId, matchId, homeScore, awayScore);
return res.status(200).json({ success: true, winnerTeamId: result.winnerTeamId });
} catch (error) {
if (error instanceof Error) {
const knownMessages = new Set([
"Match not found.",
"Both teams must be known before submitting a result.",
"Scores must be whole numbers greater than or equal to 0.",
"Matches cannot end in a tie.",
]);

if (knownMessages.has(error.message)) {
const statusCode = error.message === "Match not found." ? 404 : 400;
return res.status(statusCode).json({ success: false, message: error.message });
}
}

console.error("Failed to update match result:", error);
return res.status(500).json({ success: false, message: "Server error" });
}
};
53 changes: 53 additions & 0 deletions backend/src/database/BracketBeaverDB.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,56 @@ INSERT INTO users (username, password, role) VALUES
('alice', 'alice123', 'organizer'),
('admin', 'admin123', 'admin');

CREATE TABLE tournaments (
tournamentID SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
sport VARCHAR(50) NOT NULL,
bracket_type VARCHAR(30) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
created_by INTEGER NOT NULL REFERENCES users(userID) ON DELETE RESTRICT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE teams (
teamID SERIAL PRIMARY KEY,
tournamentID INTEGER NOT NULL REFERENCES tournaments(tournamentID) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tournamentID, name)
);

CREATE TABLE venues (
venueID SERIAL PRIMARY KEY,
tournamentID INTEGER NOT NULL REFERENCES tournaments(tournamentID) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tournamentID, name)
);

CREATE TABLE matches (
matchID SERIAL PRIMARY KEY,
tournamentID INTEGER NOT NULL REFERENCES tournaments(tournamentID) ON DELETE CASCADE,
round_number INTEGER NOT NULL,
slot_number INTEGER NOT NULL,
match_label VARCHAR(100) NOT NULL,
home_team_id INTEGER REFERENCES teams(teamID) ON DELETE RESTRICT,
away_team_id INTEGER REFERENCES teams(teamID) ON DELETE RESTRICT,
home_source_match_id INTEGER REFERENCES matches(matchID) ON DELETE SET NULL,
away_source_match_id INTEGER REFERENCES matches(matchID) ON DELETE SET NULL,
winner_team_id INTEGER REFERENCES teams(teamID) ON DELETE SET NULL,
venue_id INTEGER NOT NULL REFERENCES venues(venueID) ON DELETE RESTRICT,
match_time TIMESTAMPTZ NOT NULL,
home_score INTEGER,
away_score INTEGER,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (home_team_id IS NULL OR away_team_id IS NULL OR home_team_id <> away_team_id),
CHECK (status IN ('pending', 'completed')),
UNIQUE (tournamentID, round_number, slot_number),
UNIQUE (tournamentID, venue_id, match_time)
);

CREATE INDEX matches_home_source_idx ON matches (home_source_match_id);
CREATE INDEX matches_away_source_idx ON matches (away_source_match_id);

80 changes: 79 additions & 1 deletion backend/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,82 @@ if (process.env.NODE_ENV !== "production") {
// DB connection pool
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
});

let schemaReadyPromise: Promise<void> | null = null;

export const ensureDatabaseSchema = async (): Promise<void> => {
if (schemaReadyPromise) {
return schemaReadyPromise;
}

schemaReadyPromise = (async () => {
await pool.query(
`ALTER TABLE matches ALTER COLUMN home_team_id DROP NOT NULL`
);
await pool.query(
`ALTER TABLE matches ALTER COLUMN away_team_id DROP NOT NULL`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS slot_number INTEGER`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS match_label VARCHAR(100)`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS home_source_match_id INTEGER REFERENCES matches(matchID) ON DELETE SET NULL`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS away_source_match_id INTEGER REFERENCES matches(matchID) ON DELETE SET NULL`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS winner_team_id INTEGER REFERENCES teams(teamID) ON DELETE SET NULL`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS home_score INTEGER`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS away_score INTEGER`
);
await pool.query(
`ALTER TABLE matches ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'pending'`
);
await pool.query(
`WITH ranked AS (
SELECT
matchID,
ROW_NUMBER() OVER (
PARTITION BY tournamentID, round_number
ORDER BY match_time ASC, matchID ASC
) AS computed_slot
FROM matches
)
UPDATE matches m
SET slot_number = ranked.computed_slot,
match_label = COALESCE(m.match_label, CONCAT('Round ', m.round_number, ' Match ', ranked.computed_slot)),
status = COALESCE(m.status, 'pending')
FROM ranked
WHERE m.matchID = ranked.matchID`
);
await pool.query(
`ALTER TABLE matches ALTER COLUMN slot_number SET NOT NULL`
);
await pool.query(
`ALTER TABLE matches ALTER COLUMN match_label SET NOT NULL`
);
await pool.query(
`CREATE UNIQUE INDEX IF NOT EXISTS matches_tournament_round_slot_idx
ON matches (tournamentID, round_number, slot_number)`
);
await pool.query(
Comment thread
rapidcoderx marked this conversation as resolved.
`CREATE INDEX IF NOT EXISTS matches_home_source_idx
ON matches (home_source_match_id)`
);
await pool.query(
`CREATE INDEX IF NOT EXISTS matches_away_source_idx
ON matches (away_source_match_id)`
);
Comment thread
rapidcoderx marked this conversation as resolved.
})();

return schemaReadyPromise;
};
Loading