From 70fa701fda375647a53f3e0e158becdbc03f47fa Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 13 May 2019 22:22:45 +0200 Subject: [PATCH] Started migration to database - added table creation script for bingo - added sql scripts for bingo - added data management class for bingo - added libs with utils and global variables --- CHANGELOG.md | 4 + app.js | 166 +++++++------- graphql/bingo.graphql | 94 ++++---- lib/globals.js | 15 ++ lib/utils.js | 39 ++++ routes/bingo.js | 265 ++++++++++++++++++++++- sql/bingo/clearExpired.sql | 50 +++++ sql/bingo/createBingoTables.sql | 69 ++++++ sql/bingo/queries.yaml | 107 +++++++++ sql/{createSessionTable.sql => init.sql} | 7 + 10 files changed, 687 insertions(+), 129 deletions(-) create mode 100644 lib/globals.js create mode 100644 lib/utils.js create mode 100644 sql/bingo/clearExpired.sql create mode 100644 sql/bingo/createBingoTables.sql create mode 100644 sql/bingo/queries.yaml rename sql/{createSessionTable.sql => init.sql} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6f65e..1e50695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - sql-file directory `sql` - LICENSE.md (GPL v3) - eslint to dev dependencys +- table creation script for bingo +- sql scripts for bingo +- data management class for bingo +- libs with utils and global variables ## Changed diff --git a/app.js b/app.js index 858aac3..771587b 100644 --- a/app.js +++ b/app.js @@ -1,106 +1,98 @@ const createError = require('http-errors'), - express = require('express'), - path = require('path'), - cookieParser = require('cookie-parser'), - logger = require('morgan'), - compileSass = require('express-compile-sass'), - session = require('express-session'), - pg = require('pg'), - pgSession = require('connect-pg-simple')(session), - fsx = require('fs-extra'), - yaml = require('js-yaml'), - graphqlHTTP = require('express-graphql'), - { buildSchema } = require('graphql'), - { importSchema } = require('graphql-import'), + express = require('express'), + path = require('path'), + cookieParser = require('cookie-parser'), + logger = require('morgan'), + compileSass = require('express-compile-sass'), + session = require('express-session'), + pgSession = require('connect-pg-simple')(session), + fsx = require('fs-extra'), + graphqlHTTP = require('express-graphql'), + {buildSchema} = require('graphql'), + {importSchema} = require('graphql-import'), - indexRouter = require('./routes/index'), - usersRouter = require('./routes/users'), - riddleRouter = require('./routes/riddle'), - bingoRouter = require('./routes/bingo'); + globals = require('./lib/globals'), + settings = globals.settings, -let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + indexRouter = require('./routes/index'), + usersRouter = require('./routes/users'), + riddleRouter = require('./routes/riddle'), + bingoRouter = require('./routes/bingo'); -if (fsx.existsSync('config.yaml')) - Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); async function init() { - // grapql default resolver - let graphqlResolver = (request, response) => { - return { - time: Date.now(), - bingo: bingoRouter.graphqlResolver(request, response) + // grapql default resolver + let graphqlResolver = async (request, response) => { + return { + time: Date.now(), + bingo: await bingoRouter.graphqlResolver(request, response) + }; }; - }; - // database setup - 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 - }); - await pgPool.query(fsx.readFileSync('./sql/createSessionTable.sql', 'utf-8')); + // database setup + let pgPool = globals.pgPool; + await pgPool.query(fsx.readFileSync('./sql/init.sql', 'utf-8')); + await bingoRouter.init(); - let app = express(); + let app = express(); - // view engine setup - app.set('views', path.join(__dirname, 'views')); - app.set('view engine', 'pug'); - app.set('trust proxy', 1); + // view engine setup + app.set('views', path.join(__dirname, 'views')); + app.set('view engine', 'pug'); + app.set('trust proxy', 1); - app.use(logger('dev')); - app.use(express.json()); - app.use(express.urlencoded({ extended: false })); - app.use(cookieParser()); - app.use(session({ - store: new pgSession({ - pool: pgPool, - tableName: 'user_sessions' - }), - secret: settings.sessions.secret, - resave: false, - saveUninitialized: true, - cookie: { - maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days - } - })); - app.use('/sass', compileSass({ - root: './public/stylesheets/sass', - sourceMap: true, - watchFiles: true, - logToConsole: true - })); - app.use(express.static(path.join(__dirname, 'public'))); + app.use(logger('dev')); + app.use(express.json()); + app.use(express.urlencoded({extended: false})); + app.use(cookieParser()); + app.use(session({ + store: new pgSession({ + pool: pgPool, + tableName: 'user_sessions' + }), + secret: settings.sessions.secret, + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 7 * 24 * 60 * 60 * 1000 // maxAge 7 days + } + })); + app.use('/sass', compileSass({ + root: './public/stylesheets/sass', + sourceMap: true, + watchFiles: true, + logToConsole: true + })); + app.use(express.static(path.join(__dirname, 'public'))); - app.use('/', indexRouter); - app.use('/users', usersRouter); - app.use(/\/riddle(\/.*)?/, riddleRouter); - app.use('/bingo', bingoRouter); - app.use('/graphql', graphqlHTTP((request, response) => { - return { - schema: buildSchema(importSchema('./graphql/schema.graphql')), - rootValue: graphqlResolver(request, response), - context: {session: request.session}, - graphiql: true - }; - })); + app.use('/', indexRouter); + app.use('/users', usersRouter); + app.use(/\/riddle(\/.*)?/, riddleRouter); + app.use('/bingo', bingoRouter); + app.use('/graphql', graphqlHTTP(async (request, response) => { + return await { + schema: buildSchema(importSchema('./graphql/schema.graphql')), + rootValue: await graphqlResolver(request, response), + context: {session: request.session}, + graphiql: true + }; + })); // catch 404 and forward to error handler - app.use(function(req, res, next) { - next(createError(404)); - }); + app.use(function (req, res, next) { + next(createError(404)); + }); // error handler - app.use(function(err, req, res) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + app.use(function (err, req, res) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.render('error'); - }); - return app; + // render the error page + res.status(err.status || 500); + res.render('error'); + }); + return app; } module.exports = init; diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 36ec33a..fd2b2fb 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -1,114 +1,128 @@ type BingoMutation { + "creates a lobby for a game and returns the lobby id" + createLobby: ID - # creates a game of bingo and returns the game id + "joins a lobby and returns the connection" + joinLobby(input: joinLobbyInput): PlayerLobbyConnection + + "creates a game of bingo and returns the game" createGame(input: CreateGameInput!): BingoGame - # submit a bingo to the active game session + "submit a bingo to the active game session" submitBingo: BingoGame - # toggle a word (heared or not) on the sessions grid + "toggle a word (heared or not) on the sessions grid" toggleWord(input: WordInput!): BingoGrid - # set the username of the current session + "set the username of the current session" setUsername(input: UsernameInput!): BingoUser - # recreates the active game to a follow-up + "recreates the active game to a follow-up" createFollowupGame: BingoGame - # sends a message to the current sessions chat + "sends a message to the current sessions chat" sendChatMessage(input: MessageInput!): ChatMessage } type BingoQuery { - # Returns the currently active bingo game + "returns the currently active bingo game" gameInfo(input: IdInput): BingoGame - # If there is a bingo in the fields. + "if there is a bingo in the fields." checkBingo: Boolean - # Returns the grid of the active bingo game + "returns the grid of the active bingo game" activeGrid: BingoGrid } +type PlayerLobbyConnection { + + "the id of the player" + playerId: ID! + + "the id of the lobby" + lobbyId: ID! +} + type BingoGame { - # the id of the bingo game + "the id of the bingo game" id: ID! - # the words used in the bingo game + "the words used in the bingo game" words: [String]! - # the size of the square-grid + "the size of the square-grid" gridSize: Int - # an array of players active in the bingo game + "an array of players active in the bingo game" players(input: IdInput): [BingoUser] # the player-ids that scored a bingo bingos: [String]! - # if the game has already finished + "if the game has already finished" finished: Boolean - # the id of the followup game if it has been created + "the id of the followup game if it has been created" followup: ID - # Returns the last n chat-messages + "returns the last n chat-messages" getMessages(input: MessageQueryInput): [ChatMessage!] } type BingoUser { - # the id of the bingo user + "the id of the bingo user" id: ID! - # the id of the currently active bingo game + "the id of the currently active bingo game" game: ID - # the name of the user + "the name of the user" username: String } type BingoGrid { - # the grid represented as string matrix + "the grid represented as string matrix" wordGrid: [[String]]! - # the grid represented as bingo field matrix + "the grid represented as bingo field matrix" fieldGrid: [[BingoField]]! - # if there is a bingo + "if there is a bingo" bingo: Boolean } type BingoField { - # the word contained in the bingo field + "the word contained in the bingo field" word: String - # if the word was already heared + "if the word was already heared" submitted: Boolean! - # the base64 encoded word + "the base64 encoded word" base64Word: String } type ChatMessage { - # the id of the message + "the id of the message" id: ID! - # the content of the message + "the content of the message" content: String! # the content of the message rendered by markdown-it htmlContent: String - # the type of the message + "the type of the message" type: MessageType! - # the username of the sender + "the username of the sender" username: String # the time the message was send (in milliseconds) @@ -119,18 +133,24 @@ type ChatMessage { # input Types # # # +input joinLobbyInput { + + "the id of the lobby to join" + lobbyId: ID! +} + input CreateGameInput { - # the words used to fill the bingo grid + "the words used to fill the bingo grid" words: [String!]! - # the size of the bingo grid + "the size of the bingo grid" size: Int! = 3 } input WordInput { - # the normal word string + "the normal word string" word: String # the base64-encoded word @@ -139,28 +159,28 @@ input WordInput { input UsernameInput { - # the username string + "the username string" username: String! } input IdInput { - # the id + "the id" id: ID! } input MessageInput { - # the message + "the message" message: String! } input MessageQueryInput { - # search for a specific id + "search for a specific id" id: ID - # get the last n messages + "get the last n messages" last: Int = 10 } diff --git a/lib/globals.js b/lib/globals.js new file mode 100644 index 0000000..29cb7ad --- /dev/null +++ b/lib/globals.js @@ -0,0 +1,15 @@ +const utils = require('./utils'), + pg = require('pg'); + +const settings = utils.readSettings('.'); + +Object.assign(exports, { + settings: settings, + pgPool: new pg.Pool({ + host: settings.postgres.host, + port: settings.postgres.port, + user: settings.postgres.user, + password: settings.postgres.password, + database: settings.postgres.database + }) +}); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..dbc8264 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,39 @@ +const yaml = require('js-yaml'), + fsx = require('fs-extra'); + + +/** + * Parses the `queries.yaml` file in the path. queries.yaml-format: + * exports: {List} - query keys to export + * + * queryKey: + * file: {String} name of sql-file if the sql is stored in a file. + * sql: {String} pure sql if it is not stored in a file. Will be replaced by file contents if a file was given. + * @param path {String} - the path where the queries.yaml file is stored + */ +function parseSqlYaml(path) { + let queries = yaml.safeLoad(fsx.readFileSync(`${path}/queries.yaml`)); + + for (let query of queries.exports) + if (queries[query].file) + queries[query].sql = fsx.readFileSync(`${path}/${queries[query].file}`, 'utf-8'); + + return queries; +} + +/** + * Reads the default-config.yaml and config.yaml in the path directory. + * @param path {String} - the directory of the settings files. + */ +function readSettings(path) { + let settings = yaml.safeLoad(fsx.readFileSync(`${path}/default-config.yaml`)); + + if (fsx.existsSync('config.yaml')) + Object.assign(settings, yaml.safeLoad(fsx.readFileSync(`${path}/config.yaml`))); + return settings; +} + +Object.assign(exports, { + parseSqlYaml: parseSqlYaml, + readSettings: readSettings +}); diff --git a/routes/bingo.js b/routes/bingo.js index fcf0bdf..76ce2a5 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -6,10 +6,227 @@ const express = require('express'), md = require('markdown-it')() .use(mdEmoji) .use(mdMark) - .use(mdSmartarrows); + .use(mdSmartarrows), + utils = require('../lib/utils'), + globals = require('../lib/globals'); +let pgPool = globals.pgPool; let bingoSessions = {}; +/** + * Class to manage the bingo data in the database. + */ +class BingoDataManager { + /** + * constructor functino + * @param postgresPool {pg.Pool} - the postgres pool + */ + constructor(postgresPool) { + this.pgPool = postgresPool; + this.queries = utils.parseSqlYaml('./sql/bingo') + } + + async init() { + await this.pgPool.query(this.queries.createTables.sql); + setInterval(async () => await this._databaseCleanup(), 5*60*1000); // database cleanup every 5 minutes + } + + /** + * Try-catch wrapper around the pgPool.query. + * @param query {String} - the sql query + * @param [values] {Array} - an array of values + * @returns {Promise<*>} + */ + async _queryDatabase(query, values) { + try { + return await this.pgPool.query(query, values); + } catch (err) { + console.error(`Error on query "${query}" with values ${JSON.stringify(values)}.`); + console.error(err); + console.error(err.stack); + return { + rows: null + }; + } + } + + /** + * Queries the database and returns all resulting rows + * @param query {String} - the sql query + * @param values {Array} - an array of parameters needed in the query + * @returns {Promise<*>} + * @private + */ + async _queryAllResults(query, values) { + let result = await this._queryDatabase(query, values); + return result.rows; + } + + /** + * Query the database and return the first result or null + * @param query {String} - the sql query + * @param values {Array} - an array of parameters needed in the query + * @returns {Promise<*>} + */ + async _queryFirstResult(query, values) { + let result = await this._queryDatabase(query, values); + if (result.rows.length > 0) + return result.rows[0]; + } + + /** + * Clears expired values from the database. + */ + async _databaseCleanup() { + await this._queryDatabase(this.queries.cleanup.sql); + } + + /** + * Add a player to the players table + * @param username {String} - the username of the player + * @returns {Promise<*>} + */ + async addPlayer(username) { + let result = await this._queryFirstResult(this.queries.addPlayer.sql, [username]); + if (result) + return result; + else + return {}; // makes things easier + } + + /** + * Updates the username of a player + * @param playerId {Number} - the id of the player + * @param username {String} - the new username + * @returns {Promise} + */ + async updatePlayerUsername(playerId, username) { + return await this._queryFirstResult(this.queries.updatePlayerUsername, [username, playerId]); + } + + /** + * Returns the username for a player-id + * @param playerId {Number} - the id of the player + * @returns {Promise<*>} + */ + async getPlayerUsername(playerId) { + let result = await this._queryFirstResult(this.queries.getPlayerUsername, [playerId]); + if (result) + return result.username; + } + + /** + * Updates the expiration date of a player + * @param playerId {Request} - thie id of the player + * @returns {Promise} + */ + async updatePlayerExpiration(playerId) { + await this._queryDatabase(this.queries.updatePlayerExpire.sql, [playerId]); + } + + /** + * Creates a bingo lobby. + * @param playerId + * @param gridSize + * @returns {Promise<*>} + */ + async createLobby(playerId, gridSize) { + return await this._queryFirstResult(this.queries.addLobby.sql, [playerId, gridSize]); + } + + /** + * Updates the expiration date of a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async updateLobbyExpiration(lobbyId) { + return await this._queryDatabase(this.queries.updateLobbyExpire.sql, [lobbyId]); + } + + /** + * Checks if a player is in a lobby. + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async getPlayerInLobby(playerId, lobbyId) { + return (await this._queryFirstResult(this.queries.getPlayerInLobby.sql, [playerId, lobbyId])); + } + + /** + * Adds a player to a lobby. + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async addPlayerToLobby(playerId, lobbyId) { + let entry = await this.getPlayerInLobby(playerId, lobbyId); + if (entry) + return entry; + else + return await this._queryFirstResult(this.queries.addPlayerToLobby.sql, [playerId, lobbyId]); + } + + /** + * Removes a player from a lobbby + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async removePlayerFromLobby(playerId, lobbyId) { + return await this._queryFirstResult(this.queries.removePlayerFromLobby.sql, [playerId, lobbyId]); + } + + /** + * Adds a word to a lobby + * @param lobbyId {Number} - the id of the lobby + * @param word {Number} - the id of the word + * @returns {Promise} + */ + async addWordToLobby(lobbyId, word) { + return await this._queryFirstResult(this.queries.addWord.sql, [lobbyId, word]); + } + + /** + * Returns all words used in a lobby + * @param lobbyId + * @returns {Promise} + */ + async getWordsForLobby(lobbyId) { + return await this._queryAllResults(this.queries.getWordsForLobby.sql, [lobbyId]); + } + + /** + * Adds a grid for a user to a lobby + * @param lobbyId {Number} - the id of the lobby + * @param playerId {Number} - the id of the user + * @returns {Promise} + */ + async addGrid(lobbyId, playerId) { + return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId]); + } + + /** + * Adds a word to a grid with specific location + * @param gridId {Number} - the id of the gird + * @param wordId {Number} - the id of the word + * @param row {Number} - the number of the row + * @param column {Number} - the number of the column + */ + async addWordToGrid(gridId, wordId, row, column) { + return await this._queryFirstResult(this.queries.addWordToGrid.sql, [gridId, wordId, row, column]); + } + + /** + * Returns all words in the grid with location + * @param gridId {Number} - the id of the grid + * @returns {Promise<*>} + */ + async getWordsInGrid(gridId) { + return await this._queryAllResults(this.queries.getWordsInGrid.sql, [gridId]); + } +} + class BingoSession { /** * constructor @@ -311,12 +528,21 @@ function checkBingo(bingoGrid) { return false; } + // -- Router stuff -router.use((req, res, next) => { + +let bdm = new BingoDataManager(pgPool); + +router.init = async () => { + await bdm.init(); +}; + +router.use(async (req, res, next) => { if (!req.session.bingoUser) req.session.bingoUser = new BingoUser(); - + if (req.session.bingoPlayerId) + await bdm.updatePlayerExpiration(req.session.bingoPlayerId); next(); }); @@ -347,10 +573,13 @@ router.get('/', (req, res) => { } }); -router.graphqlResolver = (req, res) => { +router.graphqlResolver = async (req, res) => { + if (req.session.bingoPlayerId) + await bdm.updatePlayerExpiration(req.session.bingoPlayerId); let bingoUser = req.session.bingoUser || new BingoUser(); let gameId = req.query.game || bingoUser.game || null; let bingoSession = bingoSessions[gameId]; + return { // queries gameInfo: ({input}) => { @@ -366,6 +595,28 @@ router.graphqlResolver = (req, res) => { return bingoUser.grids[gameId]; }, // mutation + createLobby: async ({input}) => { + let gridSize = (input && input.gridSize)? input.gridSize : 3; + let lobby = await bdm.createLobby(req.session.bingoPlayerId, gridSize); + if (lobby && lobby.id) + return lobby.id; + else + res.status(500); + }, + joinLobby: async ({input}) => { + if (input.lobbyId) { + let entry = await bdm.addPlayerToLobby(req.session.bingoPlayerId, input.lobbyId); + if (entry && entry.lobby_id && entry.player_id) + return { + lobbyId: entry.lobby_id, + playerId: entry.player_id + }; + else + res.status(500); + } else { + res.status(400); + } + }, createGame: ({input}) => { let words = input.words.filter((el) => { // remove empty strings and non-types from word array return (!!el && el.length > 0); @@ -413,10 +664,14 @@ router.graphqlResolver = (req, res) => { res.status(400); } }, - setUsername: ({input}) => { + setUsername: async ({input}) => { if (input.username) { bingoUser.username = input.username.substring(0, 30); // only allow 30 characters + if (!req.session.bingoPlayerId) + req.session.bingoPlayerId = (await bdm.addPlayer(input.username)).id; + else + await bdm.updatePlayerUsername(req.session.bingoPlayerId, input.username); if (bingoSession) bingoSession.addUser(bingoUser); diff --git a/sql/bingo/clearExpired.sql b/sql/bingo/clearExpired.sql new file mode 100644 index 0000000..9a0fb9a --- /dev/null +++ b/sql/bingo/clearExpired.sql @@ -0,0 +1,50 @@ +/*-- remove grid-word connections for expired lobbys +DELETE FROM bingo.grid_words +WHERE EXISTS( + SELECT grids.lobby_id FROM bingo.grids + WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = grids.lobby_id + AND NOW() > lobbys.expire + ) +); + +-- remove grids for expired lobbys +DELETE FROM bingo.grids +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = grids.lobby_id + AND NOW() > lobbys.expire +); + +-- remove words for expired lobbys +DELETE FROM bingo.words +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = words.lobby_id + AND NOW() > lobbys.expire +); + +-- remove lobby-player connections for expired lobbys or players +DELETE FROM bingo.lobby_players +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = lobby_players.lobby_id + AND NOW() > lobbys.expire +) OR EXISTS ( + SELECT players.id FROM bingo.players + WHERE players.id = lobby_players.player_id + AND NOW() > players.expire +); +*/ +-- remove expired lobbys +DELETE FROM bingo.lobbys +WHERE NOW() > lobbys.expire; + +-- remove expired players +DELETE FROM bingo.players +WHERE NOW() > players.expire; +/*AND NOT EXISTS ( + SELECT lobbys.admin_id FROM bingo.lobbys + WHERE lobbys.admin_id = players.id +);*/ diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql new file mode 100644 index 0000000..dd3c193 --- /dev/null +++ b/sql/bingo/createBingoTables.sql @@ -0,0 +1,69 @@ +-- players table +CREATE TABLE IF NOT EXISTS bingo.players ( + id serial UNIQUE PRIMARY KEY, + username varchar(32) NOT NULL, + expire timestamp DEFAULT (NOW() + interval '24 hours' ) +); + +-- lobbys table +CREATE TABLE IF NOT EXISTS bingo.lobbys ( + id serial UNIQUE PRIMARY KEY, + admin_id serial references bingo.players(id) ON DELETE SET NULL, + grid_size integer DEFAULT 3, + expire timestamp DEFAULT (NOW() + interval '1 hour' ) +); + +-- lobbys-players table +CREATE TABLE IF NOT EXISTS bingo.lobby_players ( + player_id serial references bingo.players(id) ON DELETE CASCADE, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + score integer DEFAULT 0, + PRIMARY KEY (player_id, lobby_id) +); + +-- words table +CREATE TABLE IF NOT EXISTS bingo.words ( + id serial UNIQUE PRIMARY KEY, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + heared integer DEFAULT 0, + content varchar(254) NOT NULL +); + +-- grids table +CREATE TABLE IF NOT EXISTS bingo.grids ( + id serial UNIQUE PRIMARY KEY, + player_id serial references bingo.players(id) ON DELETE CASCADE, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE +); + +-- grids_words table +CREATE TABLE IF NOT EXISTS bingo.grid_words ( + grid_id serial references bingo.grids(id) ON DELETE CASCADE, + word_id serial references bingo.words(id) ON DELETE CASCADE, + grid_row integer NOT NULL, + grid_column integer NOT NULL, + PRIMARY KEY (grid_id, word_id) +); + +-- messages table +CREATE TABLE IF NOT EXISTS bingo.messages ( + id serial UNIQUE PRIMARY KEY, + content varchar(255) NOT NULL, + player_id serial references bingo.players(id) ON DELETE SET NULL, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + type varchar(8), + created timestamp DEFAULT NOW() +); + +-- rounds table +CREATE TABLE IF NOT EXISTS bingo.rounds ( + id serial UNIQUE PRIMARY KEY, + start timestamp DEFAULT NOW(), + finish timestamp, + status varchar(8) DEFAULT 'undefined', + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + winner serial references bingo.players(id) ON DELETE SET NULL +); + +-- altering +ALTER TABLE bingo.lobbys ADD COLUMN IF NOT EXISTS current_round serial references bingo.rounds(id) ON DELETE SET NULL; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml new file mode 100644 index 0000000..d8e3efa --- /dev/null +++ b/sql/bingo/queries.yaml @@ -0,0 +1,107 @@ +# a file to list sql queries by names + +exports: # loaded from file + - createTables + - cleanup + +# create the needed bingo tables +createTables: + file: createBingoTables.sql + +# clears expired values +cleanup: + file: clearExpired.sql + +# Add a player to the database +# params: +# - {String} - the username of the player +addPlayer: + sql: INSERT INTO bingo.players (username) VALUES ($1) RETURNING *; + +# Updates the username of a player +# params: +# - {String} - the new username of the player +# - {Number} - the id of the player +updatePlayerUsername: + sql: UPDATE bingo.players SET players.username = $1 WHERE players.id = $2 RETURNING *; + +# Selects the username for a player id +# params: +# - {Number} - the id of the player +getPlayerUsername: + sql: SELECT players.username FROM bingo.players WHERE id = $1; + +# updates the expiration timestamp of the player +# params: +# - {Number} - the id of the player +updatePlayerExpire: + sql: UPDATE bingo.players SET expire = (NOW() + interval '24 hours') WHERE id = $1; + +# adds a lobby to the database +# params: +# - {Number} - the id of the admin player +# - {Number} - the size of the grid +addLobby: + sql: INSERT INTO bingo.lobbys (admin_id, grid_size) VALUES ($1, $2) RETURNING *; + +# updates expiration timestamp of the lobby +# params: +# - {Number} - the id of the lobby +updateLobbyExpire: + sql: UPDATE bingo.lobbys SET expire = (NOW() + interval '1 hours') WHERE id = $1; + +# inserts a player into a lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +addPlayerToLobby: + sql: INSERT INTO bingo.lobby_players (player_id, lobby_id) VALUES ($1, $2); + +# removes a player from a lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +removePlayerFromLobby: + sql: REMOVE FROM bingo.lobby_players WHERE player_id = $1 AND lobby_id = $2; + +# returns the entry of the player and lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +getPlayerInLobby: + sql: SELECT * FROM bingo.lobby_players lp WHERE lp.player_id = $1 AND lp.lobby_id = $2; + +# adds a word to the database +# params: +# - {Number} - the id of the lobby where the word is used +# - {String} - the word itself +addWord: + sql: INSERT INTO bingo.words (lobby_id, content) VALUES ($1, $2) RETURNING *; + +# returns all words for a bingo game (lobby) +# params: +# - {Number} - the id of the bingo lobby +getWordsForLobby: + sql: SELECT * FROM bingo.words WHERE words.lobby_id = $1; + +# adds a grid to the database +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +addGrid: + sql: INSERT INTO bingo.grids (player_id, lobby_id) VALUES ($1, $2) RETURNING *; + +# inserts grid-word connections into the database +# params: +# - {Number} - the id of the grid +# - {Number} - the id of the word +# - {Number} - the row of the word +# - {Number} - the column of the word +addWordToGrid: + sql: INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES ($1, $2, $3, $4) RETURNING *; + +# returns all words for a players grid +# params: +# - {Number} - the id of the grid +getWordsForGridId: + sql: SELECT * FROM bingo.grid_words, bingo.words WHERE grid_words.grid_id = $1 AND words.id = grid_words.word_id; diff --git a/sql/createSessionTable.sql b/sql/init.sql similarity index 70% rename from sql/createSessionTable.sql rename to sql/init.sql index 50b1234..fbbfd62 100644 --- a/sql/createSessionTable.sql +++ b/sql/init.sql @@ -1,3 +1,10 @@ +-- schemas + +CREATE SCHEMA IF NOT EXISTS bingo; + +-- public tables + +-- creates the Session table CREATE TABLE IF NOT EXISTS "user_sessions" ( "sid" varchar NOT NULL COLLATE "default", "sess" json NOT NULL,