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
pull/15/head
Trivernis 6 years ago
parent f026cd6cca
commit 70fa701fda

@ -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

@ -5,41 +5,33 @@ const createError = require('http-errors'),
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'),
globals = require('./lib/globals'),
settings = globals.settings,
indexRouter = require('./routes/index'),
usersRouter = require('./routes/users'),
riddleRouter = require('./routes/riddle'),
bingoRouter = require('./routes/bingo');
let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml'));
if (fsx.existsSync('config.yaml'))
Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml')));
async function init() {
// grapql default resolver
let graphqlResolver = (request, response) => {
let graphqlResolver = async (request, response) => {
return {
time: Date.now(),
bingo: bingoRouter.graphqlResolver(request, response)
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'));
let pgPool = globals.pgPool;
await pgPool.query(fsx.readFileSync('./sql/init.sql', 'utf-8'));
await bingoRouter.init();
let app = express();
@ -61,7 +53,7 @@ async function init() {
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days
maxAge: 7 * 24 * 60 * 60 * 1000 // maxAge 7 days
}
}));
app.use('/sass', compileSass({
@ -76,10 +68,10 @@ async function init() {
app.use('/users', usersRouter);
app.use(/\/riddle(\/.*)?/, riddleRouter);
app.use('/bingo', bingoRouter);
app.use('/graphql', graphqlHTTP((request, response) => {
return {
app.use('/graphql', graphqlHTTP(async (request, response) => {
return await {
schema: buildSchema(importSchema('./graphql/schema.graphql')),
rootValue: graphqlResolver(request, response),
rootValue: await graphqlResolver(request, response),
context: {session: request.session},
graphiql: true
};

@ -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
}

@ -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
})
});

@ -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
});

@ -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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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);

@ -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
);*/

@ -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;

@ -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;

@ -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,
Loading…
Cancel
Save