diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 8dabbda..cfe1e46 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -3,7 +3,7 @@ import {Pool, PoolClient, QueryConfig, QueryResult} from "pg"; const logger = globals.logger; -class SqlTransaction { +export class SqlTransaction { constructor(private client: PoolClient) { } @@ -44,7 +44,7 @@ class SqlTransaction { } } -class QueryHelper { +export class QueryHelper { private pool: Pool; constructor(pgPool: Pool) { @@ -96,4 +96,19 @@ class QueryHelper { } } -export default QueryHelper +/** + * Returns the parameterized value sql for inserting + * @param columnCount + * @param rowCount + * @param [offset] + */ +export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string { + let sql = ''; + for (let i = 0; i < rowCount; i++) { + sql += '('; + for (let j = 0; j < columnCount; j++) + sql += `$${(i*columnCount)+j+1+offset},`; + sql = sql.replace(/,$/, '') + '),'; + } + return sql.replace(/,$/, ''); +} diff --git a/src/lib/Route.ts b/src/lib/Route.ts index e040678..5544f54 100644 --- a/src/lib/Route.ts +++ b/src/lib/Route.ts @@ -1,5 +1,5 @@ import {Router} from 'express'; -import {Server} from 'socket.io'; +import {Namespace, Server} from 'socket.io'; /** * Abstract Route class to be implemented by each route. @@ -7,13 +7,14 @@ import {Server} from 'socket.io'; * for each route. */ abstract class Route { - private io?: Server; + protected io?: Server; + protected ions?: Namespace; public router?: Router; - public resolver?: object; abstract async init(...params: any): Promise; abstract async destroy(...params: any): Promise; + abstract async resolver(request: any, response: any): Promise; } export default Route; diff --git a/src/lib/bingo-wrappers.ts b/src/lib/bingo-wrappers.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/bingo/Lobby.ts b/src/lib/bingo/Lobby.ts new file mode 100644 index 0000000..06424b7 --- /dev/null +++ b/src/lib/bingo/Lobby.ts @@ -0,0 +1,59 @@ +import {BingoSql, LobbyRow} from "../db/BingoSql"; +import {Room} from 'socket.io'; + +export class BingoLobby { + public readonly room: Room; + + private lobbyId: number; + private adminId: number; + private gridSize: number; + private currentRoundId: number; + private initialized: boolean = false; + + constructor(private bingoSql: BingoSql, adminId: number, room: Room, id?: number) { + this.adminId = adminId; + this.lobbyId = id; + this.room = room; + } + + /** + * Readonly for others to avoid complications + */ + public get id() { + return this.lobbyId; + } + + /** + * Initializes the lobby. Creates one if no id is assigned. + */ + public async init() { + let data: LobbyRow; + if (!this.id) { + data = await this.bingoSql.createLobby(this.adminId); + } else { + data = await this.bingoSql.getLobby(this.id); + } + this.assignData(data); + this.initialized = true; + } + + /** + * Reloads the lobby data if neccessary or forced. + * @param force + */ + private async loadData(force: boolean) { + if(!this.initialized || force) + await this.init(); + } + + /** + * Loads the data from the row into the parameters. + * @param row + */ + private assignData(row: LobbyRow) { + this.lobbyId = row.id; + this.adminId = row.admin_id; + this.gridSize = row.grid_size; + this.currentRoundId = row.current_round; + } +} diff --git a/src/lib/bingo/bingo-wrappers.ts b/src/lib/bingo/bingo-wrappers.ts new file mode 100644 index 0000000..c00ff6a --- /dev/null +++ b/src/lib/bingo/bingo-wrappers.ts @@ -0,0 +1,18 @@ +export class GridFieldWrapper { + public row: any; + public column: any; + public submitted: boolean; + public word: any; + public grid: any; + /** + * @param row {Object} - the resulting row + */ + constructor(row: any) { + this.row = row.grid_row; + this.column = row.grid_column; + this.submitted = row.submitted; + // TODO + //this.word = new WordWrapper(row.word_id, row); + //this.grid = new GridWrapper(row.grid_id); + } +} diff --git a/src/lib/db/BingoSql.ts b/src/lib/db/BingoSql.ts index 0268ad9..800328d 100644 --- a/src/lib/db/BingoSql.ts +++ b/src/lib/db/BingoSql.ts @@ -1,4 +1,336 @@ -class BingoSql { - constructor() { +import {Pool} from "pg"; +import {QueryHelper, buildSqlParameters} from '../QueryHelper'; + +enum MessageType { + USER = "USER", + INFO = "INFO", + ERROR = "ERROR" +} + +enum RoundStatus { + BUILDING = "BUILDING", + FINISHED = "FINISHED", + ACTIVE = "ACTIVE" +} + +export type PlayerRow = {id: number, username: string, expire: string}; +export type LobbyRow = {id: number, admin_id: number, grid_size: number, current_round: number, expire: string}; +export type LobbyPlayerRow = {player_id: number, lobby_id: number, score: number}; +export type WordRow = {id: number|BigInteger, lobby_id: number, heared: number, content: string}; +export type MessageRow = {id: number|BigInteger, content: string, player_id: number, lobby_id: number, type: MessageType, created: string}; +export type RoundRow = {id: number, start: string, finish: string, status: RoundStatus, lobby_id: number, winner: number}; +export type GridRow = {id: number, player_id: number, lobby_id: number, round_id: number}; +export type GridWordRow = {grid_id: number, word_id: number, grid_row: number, grid_column: number, submitted: boolean}; + +interface BingoDatabaseInterface { + addPlayer(name: string): Promise; + getPlayer(playerId: number): Promise; + updatePlayerUsername(playerId: number, name: string): Promise; + updatePlayerExpiration(playerId: number): Promise; + getPlayerWins(playerId: number, lobbyId: number): Promise + + createLobby(playerId: number, gridSize: number): Promise; + setLobbyGridSize(lobbyId: number, gridSize: number): Promise; + setLobbyRound(lobbyId: number, roundId: number): Promise; + addPlayerToLobby(playerId: number, lobbyId: number): Promise; + removePlayerFromLobby(playerId: number, lobbyId: number): Promise; + getLobby(lobbyId: number): Promise; + getLobbyIds(): Promise; + checkPlayerInLobby(playerId: number, lobbyId: number): Promise; + getLobbyMembers(lobbyId: number): Promise; + getLobbyMessages(lobbyId: number, limit: number): Promise; + updateLobbyExpiration(lobbyId: number): Promise; + clearGrids(lobbyId: number): Promise; + clearWords(lobbyId: number): Promise; + + addWord(lobbyId: number, word: string): Promise; + removeWord(lobbyid: number, wordId: number): Promise; + getWords(lobbyId: number): Promise; + getWord(wordId: number): Promise; + + getRounds(lobbyId: number): Promise; + getRound(roundId: number): Promise; + updateRound(roundId: number, status: RoundStatus): Promise; + setRoundFinished(roundId: number): Promise; + setRoundWinner(roundId: number, playerId: number): Promise; + + addGrid(lobbyId: number, playerId: number, roundId: number): Promise; + addGridWords(words: GridWordRow[]): Promise; + getGridWords(gridId: number): Promise<(GridWordRow & WordRow)[]>; + getGridField(gridId: number, row: number, column: number): Promise<(GridWordRow & WordRow)>; + + addPlayerMessage(lobbyId: number, playerId: number, content: string): Promise; + addInfoMessage(lobbyId: number, content: string): Promise; + editMessage(messageId: number, content: string): Promise; + deleteMessage(messageId: number): Promise; + getMessage(messageId: number): Promise; +} + +export class BingoSql implements BingoDatabaseInterface{ + + private queryHelper: QueryHelper; + + constructor(pgPool: Pool) { + this.queryHelper = new QueryHelper(pgPool); + } + + async addPlayer(name: string): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.players (username) VALUES ($1) RETURNING *;", + values: [name] + }); + } + + async getPlayer(playerId: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * FROM bingo.players WHERE id = $1;", + values: [playerId] + }); + } + + async updatePlayerUsername(playerId: number, name: string): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.players SET username = $1 WHERE id = $2 RETURNING *;", + values: [name, playerId] + }); + } + + async updatePlayerExpiration(playerId: number): Promise { + await this.queryHelper.all({ + name: 'update-player-expire', + text: "UPDATE bingo.players SET expire = (NOW() + interval '24 hours') WHERE id = $1;", + values: [playerId] + }); + } + async getPlayerWins(playerId: number, lobbyId: number): Promise { + return (await this.queryHelper.first({ + name: "select-player-wins", + text: " SELECT COUNT(*) wins FROM bingo.rounds WHERE rounds.lobby_id = $1 AND rounds.winner = $2;", + values: [lobbyId, playerId] + })).wins; + } + + async createLobby(playerId: number, gridSize: number=3): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.lobbys (admin_id, grid_size) VALUES ($1, $2) RETURNING *;", + values: [playerId, gridSize] + }); + } + + async setLobbyGridSize(lobbyId: number, gridSize: number): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.lobbys (admin_id, grid_size) VALUES ($1, $2) RETURNING *;", + values: [lobbyId, gridSize] + }); + } + + async setLobbyRound(lobbyId: number, roundId: number): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.lobbys SET current_round = $2 WHERE id = $1 RETURNING *;", + values: [lobbyId, roundId] + }); + } + + async addPlayerToLobby(playerId: number, lobbyId: number): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.lobby_players (player_id, lobby_id) VALUES ($1, $2) RETURNING *;", + values: [playerId, lobbyId] + }); + } + + async removePlayerFromLobby(playerId: number, lobbyId: number): Promise { + await this.queryHelper.all({ + text: "DELETE FROM bingo.lobby_players WHERE player_id = $1 AND lobby_id = $2;", + values: [playerId, lobbyId] + }); + } + + async getLobby(lobbyId: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * FROM bingo.lobbys WHERE lobbys.id = $1;", + values: [lobbyId] + }); + } + + async getLobbyIds(): Promise { + return (await this.queryHelper.all({ + text: "SELECT lobbys.id FROM bingo.lobbys;" + })).map(x => x.id); + } + + async checkPlayerInLobby(playerId: number, lobbyId: number): Promise { + return !!await this.queryHelper.first({ + text: "SELECT * FROM bingo.lobby_players lp WHERE lp.player_id = $1 AND lp.lobby_id = $2;", + values: [playerId, lobbyId] + }); + } + + async getLobbyMembers(lobbyId: number): Promise { + return await this.queryHelper.all({ + text: "SELECT * FROM bingo.lobby_players WHERE lobby_players.lobby_id = $1;", + values: [lobbyId] + }); + } + + async getLobbyMessages(lobbyId: number, limit: number = 20): Promise { + return await this.queryHelper.all({ + text: "SELECT * FROM bingo.messages WHERE messages.lobby_id = $1 ORDER BY messages.created DESC LIMIT $2;", + values: [lobbyId, limit] + }); + } + + async updateLobbyExpiration(lobbyId: number): Promise { + await this.queryHelper.all({ + text: "UPDATE bingo.lobbys SET expire = (NOW() + interval '4 hours') WHERE id = $1 RETURNING *;", + values: [lobbyId] + }); + } + + async clearGrids(lobbyId: number): Promise { + await this.queryHelper.all({ + text: "DELETE FROM bingo.grids WHERE lobby_id = $1;", + values: [lobbyId] + }); + } + + async clearWords(lobbyId: number): Promise { + await this.queryHelper.all({ + text: "DELETE FROM bingo.words WHERE lobby_id = $1;", + values: [lobbyId] + }); + } + + async addWord(lobbyId: number, word: string): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.words (lobby_id, content) VALUES ($1, $2) RETURNING *;", + values: [lobbyId, word] + }); + } + + async removeWord(lobbyid: number, wordId: number): Promise { + await this.queryHelper.all({ + text: "DELETE FROM bingo.words WHERE lobby_id = $1 AND id = $2;", + values: [lobbyid, wordId] + }); + } + + async getWords(lobbyId: number): Promise { + return await this.queryHelper.all({ + text: "SELECT * FROM bingo.words WHERE words.lobby_id = $1;", + values: [lobbyId] + }); + } + + async getWord(wordId: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * FROM bingo.words WHERE words.id = $1;", + values: [wordId] + }); + } + + async getRounds(lobbyId: number): Promise { + return await this.queryHelper.all({ + text: "SELECT * FROM bingo.rounds WHERE rounds.lobby_id = $1;", + values: [lobbyId] + }); + } + + async getRound(roundId: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * FROM bingo.rounds WHERE rounds.id = $1;", + values: [roundId] + }); + } + + async updateRound(roundId: number, status: RoundStatus): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.rounds SET status = $2 WHERE id = $1 RETURNING *;", + values: [roundId, status] + }); + } + + async setRoundFinished(roundId: number): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.rounds SET status = 'FINISHED', finish = NOW() WHERE id = $1 RETURNING *;", + values: [roundId] + }); + } + + async setRoundWinner(roundId: number, playerId: number): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.rounds SET winner = $2 WHERE id = $1 RETURNING *;", + values: [roundId, playerId] + }); + } + + async addGrid(lobbyId: number, playerId: number, roundId: number): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.grids (player_id, lobby_id, round_id) VALUES ($1, $2, $3) RETURNING *;", + values: [playerId, lobbyId, roundId] + }); + } + + async addGridWords(words: GridWordRow[]): Promise { + let valueSql = buildSqlParameters(4, words.length, 0); + let values = []; + for (let word of words) { + values.push(word.grid_id); + values.push(word.word_id); + values.push(word.grid_row); + values.push(word.grid_column); + } + return await this.queryHelper.first({ + text: `INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES ${valueSql} RETURNING *;`, + values: values + }); + } + + async getGridWords(gridId: number): Promise<(GridWordRow & WordRow)[]> { + return await this.queryHelper.all({ + text: "SELECT * FROM bingo.grid_words, bingo.words WHERE grid_words.grid_id = $1 AND words.id = grid_words.word_id;", + values: [gridId] + }); + } + + async getGridField(gridId: number, row: number, column: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * FROM bingo.grid_words WHERE grid_words.grid_id = $1 AND grid_words.grid_row = $2 and grid_words.grid_column = $3;", + values: [gridId, row, column] + }) + } + + async addPlayerMessage(lobbyId: number, playerId: number, content: string): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *;", + values: [playerId, lobbyId, content] + }); + } + + async addInfoMessage(lobbyId: number, content: string): Promise { + return await this.queryHelper.first({ + text: "INSERT INTO bingo.messages (type, lobby_id, content) VALUES ('INFO', $1, $2) RETURNING *;", + values: [lobbyId, content] + }); + } + + async editMessage(messageId: number, content: string): Promise { + return await this.queryHelper.first({ + text: "UPDATE bingo.messages SET content = $2 WHERE id = $1 RETURNING *;", + values: [messageId, content] + }); + } + + async deleteMessage(messageId: number): Promise { + await this.queryHelper.all({ + text: "DELETE FROM bingo.messages WHERE id = $1;", + values: [messageId] + }); + } + + async getMessage(messageId: number): Promise { + return await this.queryHelper.first({ + text: "SELECT * from bingo.messages WHERE id = $1;", + values: [messageId] + }); } } diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index e69de29..e5d31bb 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -0,0 +1,19 @@ +import * as pg from "pg"; +import globals from '../globals'; +import {BingoSql} from './BingoSql'; + +const settings = globals.settings; + +export let pgPool = new pg.Pool({ + host: settings.postgres.host, + port: settings.postgres.port, + user: settings.postgres.user, + password: settings.postgres.password, + database: settings.postgres.database +}); + +namespace queries { + export const bingoSql = new BingoSql(pgPool); +} + +export default pgPool; diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 9fe4ae3..6760a19 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -1,24 +1,33 @@ -const utils = require('./utils'), - fsx = require('fs-extra'), - pg = require('pg'); +import * as utils from './utils'; +import * as fsx from 'fs-extra'; +import * as winston from 'winston'; -const settings = utils.readSettings('.'); - -Object.assign(exports, { - settings: settings, - changelog: fsx.readFileSync('CHANGELOG.md', 'utf-8'), - pgPool: new pg.Pool({ - host: settings.postgres.host, - port: settings.postgres.port, - user: settings.postgres.user, - password: settings.postgres.password, - database: settings.postgres.database - }), - cookieInfo: { +/** + * Defines global variables to be used. + */ +namespace globals { + export const settings = utils.readSettings('.'); + export const changelog: string = fsx.readFileSync('CHANGELOG.md', 'utf-8'); + export const cookieInfo = { headline: 'This website uses cookies', content: "This website uses cookies to store your session data. No data is permanently stored.", onclick: 'acceptCookies()', id: 'cookie-container', button: 'All right!' - } -}); + }; + export const logger = winston.createLogger({ + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf(({ level, message, label, timestamp }) => { + return `${timestamp} [${label}] ${level}: ${message}`; + }) + ) + }) + ] + }); +} + +export default globals; diff --git a/src/routes/bingo.ts b/src/routes/bingo.ts index de8e80a..41b571e 100644 --- a/src/routes/bingo.ts +++ b/src/routes/bingo.ts @@ -1,4 +1,5 @@ import {Router} from 'express'; +import {Namespace} from 'socket.io'; import {GraphQLError} from "graphql"; import * as markdownIt from 'markdown-it'; @@ -6,27 +7,33 @@ import * as utils from '../lib/utils'; import globals from '../lib/globals'; import Route from '../lib/Route'; -import * as wrappers from '../lib/bingo-wrappers'; +import * as wrappers from '../lib/bingo/bingo-wrappers'; +import {BingoSql} from "../lib/db/BingoSql"; +import {pgPool} from "../lib/db"; let mdEmoji = require('markdown-it-emoji'); let mdMark = require('markdown-it-mark'); let mdSmartarrows = require('markdown-it-smartarrows'); -const pgPool = globals.pgPool; - - class BingoRoute extends Route { + bingoSql: BingoSql; + constructor() { super(); this.router = Router(); - this.resolver = this.getResolver(); + this.bingoSql = new BingoSql(pgPool); } /** * Inits the Route */ - public async init() { - + public async init(ioNamespace: Namespace) { + this.ions = ioNamespace; + this.ions.on('connection', (socket) => { + socket.on('joinChat', (lobbyId: number) => { + socket.join(`lobby-${lobbyId}`); + }); + }); } /** @@ -37,14 +44,18 @@ class BingoRoute extends Route { this.resolver = null; } - private getResolver(): object { - return async (req: any, res: any) => { - let playerId = req.session.bingoPlayerId; - return { - player: () => { - return playerId; - } - }; + /** + * Graphql resolver function + * @param req + * @param res + */ + public async resolver(req: any, res: any): Promise { + let playerId = req.session.bingoPlayerId; + + return await { + player: () => { + return playerId; + } }; } } diff --git a/src/routes/index.ts b/src/routes/index.ts index 2c7cf64..e3e334d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,18 +2,22 @@ import {Router} from 'express'; import {Server} from 'socket.io'; import * as homeRouter from './home'; -import * as bingoRouter from './bingo'; +import BingoRoute from './bingo'; import changelogRouter from './changelog'; namespace routes { export const router = Router(); + const bingoRoute = new BingoRoute(); + router.use('/', homeRouter); - router.use('/bingo', bingoRouter); + router.use('/bingo', bingoRoute.router); router.use('/changelog', changelogRouter); - export const resolvers = (request: any, response: any):object => { - + export const resolvers = async (request: any, response: any): Promise => { + return await { + bingo: await bingoRoute.resolver(request, response) + }; }; export const ioListeners = (io: Server) => { diff --git a/tsconfig.json b/tsconfig.json index 2f18a6a..33b06d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "sourceMap": true, "target": "es2018", "allowJs": true, - "moduleResolution": "node" + "moduleResolution": "node", + "module": "commonjs" }, "include": [ "src/**/*"