diff --git a/mssql-wrapper.js b/mssql-wrapper.js deleted file mode 100644 index f0dc075..0000000 --- a/mssql-wrapper.js +++ /dev/null @@ -1,101 +0,0 @@ -// Vibe coded since I needed more resources on my VPS :/ - -const Database = require('better-sqlite3'); - -// 1. Connect to the file (creates it if missing) -const db = new Database('project.db'); -db.pragma('journal_mode = WAL'); // Faster, safer writes - -class FakeMssqlRequest { - constructor(db) { - this.db = db; - this.params = {}; // Stores your inputs: .input('id', 123) - } - - // Mimic the .input() method - // We ignore 'type' because SQLite is loosely typed - input(name, type, value) { - // Handle case where value is passed as 2nd arg (omitting type) - if (value === undefined) value = type; - - this.params[name] = value; - return this; // Return 'this' to allow chaining (.input().input()) - } - - // Mimic the .query() method - async query(sqlString) { - try { - let cleanSql = sqlString; - - // --- FIX 1: Handle TOP -> LIMIT --- - // Regex detects: SELECT TOP (@amount) ... OR SELECT TOP 10 ... - // It captures the value (@amount or 10) - const topRegex = /SELECT\s+TOP\s*\(?(@?\w+)\)?/i; - const topMatch = cleanSql.match(topRegex); - - if (topMatch) { - const limitValue = topMatch[1]; // Extracts '@amount' or '10' - - // 1. Remove "TOP (@amount)" from the start - cleanSql = cleanSql.replace(topRegex, 'SELECT '); - - // 2. Remove any trailing semicolon so we can append - cleanSql = cleanSql.replace(/;\s*$/, ''); - - // 3. Append LIMIT to the end - cleanSql += ` LIMIT ${limitValue}`; - } - - // 1. Compatibility Fixes (Optional but helpful) - // Replace 'GETDATE()' with SQLite's "datetime('now')" - cleanSql = cleanSql.replace(/GETDATE\(\)/gi, "datetime('now', 'localtime')") - // MS SQL: SYSUTCDATETIME() -> SQLite: datetime('now') (Already UTC) - .replace(/SYSUTCDATETIME\(\)/gi, "datetime('now')") - // --- FIX 3: Clean up Square Brackets --- - .replace(/\[/g, '"').replace(/\]/g, '"') - - // --- FIX 4: Handle String Concatenation in LIKE clauses --- - // MSSQL: LIKE '%' + @param + '%' - // SQLite: LIKE '%' || @param || '%' - // This regex looks for: LIKE '%' + @anything + '%' - .replace(/LIKE\s+'%'\s*\+\s*(@\w+)\s*\+\s*'%'/gi, "LIKE '%' || $1 || '%'"); - // 2. Determine if it's a SELECT or INSERT/UPDATE - const stmt = this.db.prepare(cleanSql); - - if (cleanSql.trim().toUpperCase().startsWith('SELECT')) { - // READ: Use .all() - const rows = stmt.all(this.params); - return { recordset: rows, rowsAffected: [rows.length] }; - } else { - // WRITE: Use .run() - const info = stmt.run(this.params); - return { recordset: [], rowsAffected: [info.changes] }; - } - } catch (err) { - console.error("SQLite Error:", err.message); - console.error("Query was:", sqlString); - throw err; - } - } -} - -// Mimic the main 'pool' object -const pool = { - connected: true, - request: () => new FakeMssqlRequest(db), - close: () => db.close() -}; - -module.exports = { - // Mimic the sql.connect() function - connect: async () => { - console.log("Connected to SQLite (via Wrapper)"); - return pool; - }, - // Export types to prevent "sql.Int is undefined" errors - Int: 'Int', - NVarChar: 'NVarChar', - Bit: 'Bit', - DateTime: 'DateTime' -}; - diff --git a/project.db-shm b/project.db-shm index 0cb38d3..cff3ff4 100644 Binary files a/project.db-shm and b/project.db-shm differ diff --git a/project.db-wal b/project.db-wal index 5e118af..3a8c7ca 100644 Binary files a/project.db-wal and b/project.db-wal differ diff --git a/server.js b/server.js index c9c5176..2812eb0 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,10 @@ const express = require("express"); -const sql = require("./mssql-wrapper"); +const sqlite = require("better-sqlite3"); const bcrypt = require("bcrypt"); const cors = require("cors"); const jwt = require("jsonwebtoken") -const dotenv = require("dotenv") +const dotenv = require("dotenv"); +const Database = require("better-sqlite3"); const serverPort = 3001; @@ -26,18 +27,8 @@ app.use(cors({ ] })); -let dbConfig = null; - -dbConfig = { - user: "server", - password: "TorusField1*", - server: "mssql.rochesterx.dev", - database: "Project", - options: { trustServerCertificate: true } -}; - -let pool = null; -setupPool(); +const db = new Database("project.db"); +db.pragma("journal_mode = WAL") dotenv.config(); @@ -46,20 +37,21 @@ if (!JWT_SECRET) { throw new Error("JWT_SECRET not set in environment."); } -async function setupPool() { - pool = await sql.connect(dbConfig); -} - app.post("/register", async (req, res) => { const { username, password, role } = req.body; try { const hash = await bcrypt.hash(password, 10); - await pool.request() - .input("username", sql.VarChar, username) - .input("hash", sql.VarChar, hash) - .input("role", sql.VarChar, role) - .query("INSERT INTO Users (Username, PasswordHash, Role, CreatedAt) VALUES (@username, @hash, @role, SYSUTCDATETIME())"); + + const inputs = { + username: username, + hash: hash, + role: role + } + const query = db.prepare("INSERT INTO Users (Username, PasswordHash, Role, CreatedAt) VALUES (@username, @hash, @role, datetime('now'))"); + query.run(inputs); + res.send({ success: true, message: "User registered" }) + } catch (err) { if (err.message.includes("Violation of UNIQUE KEY constraint")) { res.status(500).send({ success: false, message: `Username "${username}" is already taken.` }); @@ -71,14 +63,13 @@ app.post("/register", async (req, res) => { app.post("/login", async (req, res) => { const { username, password } = req.body; try { - const result = await pool.request() - .input("username", sql.VarChar, username) - .query("SELECT * FROM Users WHERE Username = @username"); + const query = db.prepare("SELECT * FROM Users WHERE Username = @username"); + const results = query.all({ username: username }); - if (result.recordset.length == 0) return res.status(400).send({ message: "User not found" }); + if (results.length == 0) return res.status(400).send({ message: "User not found" }); - const hash = result.recordset[0].PasswordHash; - const isDeleted = result.recordset[0].IsDeleted; + const hash = results[0].PasswordHash; + const isDeleted = results[0].IsDeleted; if (isDeleted === true) { return res.status(400).json({ message: "User not found (deleted)" }) @@ -87,15 +78,14 @@ app.post("/login", async (req, res) => { const match = await bcrypt.compare(password, hash); if (match){ - const token = jwt.sign(result.recordset[0], JWT_SECRET, { expiresIn: "1h" }); + const token = jwt.sign(results[0], JWT_SECRET, { expiresIn: "1h" }); res.send({ success: true, message: "Login successful", token }); - await pool.request() - .input("username", sql.VarChar, username) - .query("UPDATE Users SET LastLogin = SYSUTCDATETIME() WHERE Username = @username"); + const update = db.prepare("UPDATE Users SET LastLogin = datetime('now') WHERE Username = @username"); + update.run({ username: username }); console.log("Issued token: " + JSON.stringify(token)) } @@ -108,23 +98,23 @@ app.post("/login", async (req, res) => { app.post("/getWatchlist", authenticate, async (req, res) => { const { id } = req.body; - const watchlist = await pool.request() - .input("userID", sql.Int, req.user.Id) - .input("id", sql.Int, id) - .query(`SELECT Player.PlayerName, Player.Team, Player.Position, Player.PlayerID FROM Watch JOIN Player ON Watch.PlayerID = Player.PlayerID WHERE UserID = @userID ORDER BY Player.PlayerName`); + const query = db.prepare(`SELECT Player.PlayerName, Player.Team, Player.Position, Player.PlayerID FROM Watch JOIN Player ON Watch.PlayerID = Player.PlayerID WHERE UserID = @userID ORDER BY Player.PlayerName`); + const watchlist = query.all({ + userID: req.user.Id, + id: id + }); - res.status(200).json({ watchlist: watchlist.recordset }); + + res.status(200).json({ watchlist: watchlist }); }); app.post("/isWatched", authenticate, async (req, res) => { const { id } = req.body; - const watchlist = await pool.request() - .input("userID", sql.Int, req.user.Id) - .input("id", sql.Int, id) - .query(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); + const query = db.prepare(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); + const watchlist = query.all({ userID: req.user.Id, id: id}); - const isWatched = watchlist.recordset.some(row => row.PlayerID === parseInt(id)); + const isWatched = watchlist.some(row => row.PlayerID === parseInt(id)); res.status(200).json({ isWatched: isWatched }); }); @@ -132,30 +122,22 @@ app.post("/isWatched", authenticate, async (req, res) => { app.post("/toggleWatched", authenticate, async (req, res) => { const { id } = req.body; - const watchlist = await pool.request() - .input("userID", sql.Int, req.user.Id) - .input("id", sql.Int, id) - .query(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); - console.log(watchlist.recordset); + const query = db.prepare(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); + const watchlist = query.all({ userID: req.user.Id, id: id }); - const isWatched = watchlist.recordset.some(row => row.PlayerID === parseInt(id)); - console.log(isWatched); + const isWatched = watchlist.some(row => row.PlayerID === parseInt(id)); if (isWatched) { - const result = await pool.request() - .input("userID", sql.Int, req.user.Id) - .input("id", sql.Int, id) - .query(`DELETE FROM Watch WHERE UserID = @userID AND PlayerID = @id`); + const query = db.prepare(`DELETE FROM Watch WHERE UserID = @userID AND PlayerID = @id`); + query.run({ userID: req.user.Id, id: id }); res.status(200).json({ message: "No longer watching player" }); return; } // Otherwise, watch the player - const result = await pool.request() - .input("userID", sql.Int, req.user.Id) - .input("id", sql.Int, id) - .query(`INSERT INTO Watch (UserID, PlayerID) VALUES (@userID, @id)`); + const watchQuery = db.prepare(`INSERT INTO Watch (UserID, PlayerID) VALUES (@userID, @id)`); + watchQuery.run({ userID: req.user.Id, id: id }); res.status(200).json({ message: "Watching Player" }); }); @@ -163,38 +145,37 @@ app.post("/toggleWatched", authenticate, async (req, res) => { app.post("/getPlayers", authenticate, async (req, res) => { const { player, positions } = req.body; - const result = await pool.request() - .input("query", sql.VarChar, player) - .input("one", sql.VarChar, positions[0]) - .input("two", sql.VarChar, positions[1]) - .input("three", sql.VarChar, positions[2]) - .input("four", sql.VarChar, positions[3]) - .query(`SELECT p.PlayerID, p.PlayerName, c.TotalValue, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerName LIKE '%' + @query + '%' AND p.Position IN (@one, @two, @three, @four) ORDER BY p.PlayerName;`); + const query = db.prepare(`SELECT p.PlayerID, p.PlayerName, c.TotalValue, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerName LIKE '%' || @query || '%' AND p.Position IN (@one, @two, @three, @four) ORDER BY p.PlayerName;`); + const matches = query.all({ + query: player, + one: positions[0], + two: positions[1], + three: positions[2], + four: positions[3] + }); - res.status(200).json({ query: player, matches: result.recordset }); + res.status(200).json({ query: player, matches: matches }); }); app.post("/getHighest", authenticate, async (req, res) => { const { amount } = req.body; - const result = await pool.request() - .input("amount", sql.Int, amount) - .query(` - SELECT TOP (@amount) p.PlayerID, p.PlayerName, p.[Position], p.Team, TotalValue, TrueAvgPerYear, Years + const query = db.prepare(` + SELECT p.PlayerID, p.PlayerName, p.[Position], p.Team, TotalValue, TrueAvgPerYear, Years FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID - ORDER BY TotalValue DESC; + ORDER BY TotalValue DESC + LIMIT @amount; `); + const matches = query.all({ amount: amount }); - res.status(200).json({ matches: result.recordset }); + res.status(200).json({ matches: matches }); }); app.post("/getHighestOffense", authenticate, async (req, res) => { const { amount } = req.body; - const result = await pool.request() - .input("amount", sql.Int, amount) - .query(` -SELECT TOP (@amount) Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, + const query = db.prepare(` +SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, SUM(total_yards) AS TotalYards, SUM(passing_yards) AS PassingYards, SUM(rushing_yards) AS RushingYards, @@ -233,38 +214,36 @@ SUM(tackled_for_loss) ) - (SUM(safety) * 100.0)) / AvgPerYear AS PaydirtScore - FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID WHERE season = 2024 AND SeasonType = 'REG' GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear -ORDER BY OffenseScore DESC; +ORDER BY OffenseScore DESC +LIMIT @amount; `); + const matches = query.all({ amount: amount }); - res.status(200).json({ matches: result.recordset }); + res.status(200).json({ matches: matches }); }); app.post("/getPlayerStats", authenticate, async (req, res) => { const { playerID } = req.body; - const result = await pool.request() - .input("playerID", sql.Int, playerID) - .query(` + const query = db.prepare(` SELECT player.playerid, player.playername, season, seasontype, week, pass_attempts, complete_pass, total_yards, total_tds, interception, receptions, receiving_yards, receiving_touchdown, rush_attempts, rushing_yards, rush_touchdown, fumble fROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID WHERE Player.PlayerID = @playerID ORDER BY Season, week; `); + const matches = query.all({ playerID: playerID }) - res.status(200).json({ matches: result.recordset }); + res.status(200).json({ matches: matches }); }); app.post("/getPlayerScores", authenticate, async (req, res) => { const { playerID } = req.body; - const result = await pool.request() - .input("playerID", sql.Int, playerID) - .query(` + const query = db.prepare(` SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, Player.Height, Player.Weight, SUM(total_yards) AS TotalYards, @@ -312,9 +291,9 @@ WHERE season = 2024 AND SeasonType = 'REG' AND Player.PlayerID = @playerID GROUP BY c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, Player.Height, Player.Weight, c.EndYear, Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], c.AvgPerYear ORDER BY PaydirtScore DESC; `); - console.log(result.recordset); + const matches = query.all({ playerID: playerID }) - res.status(200).json({ match: result.recordset[0] }); + res.status(200).json({ match: matches[0] }); }); @@ -322,10 +301,8 @@ ORDER BY PaydirtScore DESC; app.post("/getHighestPaydirt", authenticate, async (req, res) => { const { amount } = req.body; - const result = await pool.request() - .input("amount", sql.Int, amount) - .query(` -SELECT TOP (@amount) Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, + const query = db.prepare(` +SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, SUM(total_yards) AS TotalYards, SUM(passing_yards) AS PassingYards, SUM(rushing_yards) AS RushingYards, @@ -369,24 +346,25 @@ SUM(tackled_for_loss) FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID WHERE season = 2024 AND SeasonType = 'REG' GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear -ORDER BY PaydirtScore DESC; +ORDER BY PaydirtScore DESC +LIMIT @amount; `); + const matches = query.all({ amount: amount }); - res.status(200).json({ matches: result.recordset }); + res.status(200).json({ matches: matches }); }); app.post("/getPlayer", authenticate, async (req, res) => { const { id } = req.body; - const result = await pool.request() - .input("query", sql.VarChar, id) - .query(`SELECT p.PlayerName, p.PlayerID, c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerID = @query`); + const query = db.prepare(`SELECT p.PlayerName, p.PlayerID, c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerID = @query`); + const matches = query.run({ query: id }); - if (result.recordset.length !== 1) { + if (matches.length !== 1) { res.status(400).json({ success: false }) return; } - res.status(200).json({ success: true, match: result.recordset[0] }); + res.status(200).json({ success: true, match: matches[0] }); }); app.post("/getInfo", authenticate, async (req, res) => { @@ -396,10 +374,8 @@ app.post("/getInfo", authenticate, async (req, res) => { }); app.post("/getCourses", authenticate, async (req, res) => { - const result = await pool.request() - .query("SELECT * FROM Courses"); - - const courses = result.recordset; + const query = db.prepare("SELECT * FROM Courses"); + const courses = query.all(); res.status(200).json(courses); }); @@ -408,18 +384,19 @@ app.post("/setInfo", authenticate, async (req, res) => { const { firstName, lastName, dob } = req.body; try { - await pool.request() - .input("username", sql.VarChar, req.user.Username) - .input("firstName", sql.NVarChar(50), firstName) - .input("lastName", sql.NVarChar(50), lastName) - .input("dob", sql.Date, dob || null) - .query(` + const query = db.prepare(` UPDATE Users SET FirstName = @firstName, LastName = @lastName, DOB = @dob WHERE Username = @username `); + query.run({ + username: req.user.Username, + firstName: firstName, + lastName: lastName, + dob: dob || null + }); } catch (error) { console.log(error); if (error.message.includes("failed for parameter 'dob'.")) { @@ -456,10 +433,11 @@ app.post("/delete", authenticate, async (req, res) => { console.log(`Deleting user ${username}`); - await pool.request() - .input("username", sql.VarChar, username) - .input("actor", sql.VarChar, actor) - .query("UPDATE Users SET IsDeleted = 1, DeletedAt = SYSUTCDATETIME(), DeletedBy = @actor WHERE Username = @username"); + const query = ("UPDATE Users SET IsDeleted = 1, DeletedAt = datetime('now'), DeletedBy = @actor WHERE Username = @username"); + query.run({ + username: username, + actor: actor + }); console.log(`User ${username} deleted`); res.status(200).json({ message: `User "${username}" deleted.` }); @@ -511,4 +489,6 @@ app.get("/register", (req, res) => { app.get("/login", (req, res) => { res.sendFile(__dirname + "/public/login.html"); }) + app.listen(serverPort, "0.0.0.0", () => console.log(`Running ${dev ? "dev " : ""}server on port ${serverPort}`)); +