From 84aff04fa10f4411836d024ec1a4a6652dcb72d6 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sat, 18 Nov 2023 05:47:50 -0800 Subject: [PATCH] Add backend routes --- app.js | 7 +++ common/utils.js | 92 +++++++++++++++++++++++++++++++ routes/gameRouter.js | 45 +++++++++++++++ routes/memberRouter.js | 40 ++++++++++++++ server/schema/members.sql | 4 +- server/schema/truths_and_lies.sql | 2 +- 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 common/utils.js create mode 100644 routes/gameRouter.js create mode 100644 routes/memberRouter.js diff --git a/app.js b/app.js index 7419cb1..54e65bc 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,10 @@ const cors = require('cors'); require('dotenv').config(); +// routes +const memberRouter = require('./routes/memberRouter'); +const gameRouter = require('./routes/gameRouter'); + const app = express(); const PORT = process.env.PORT || 3001; @@ -13,6 +17,9 @@ app.use( }), ); +app.use('/members', memberRouter); +app.use('/games', gameRouter); + app.listen(PORT, () => { console.log(`Server listening on ${PORT}`); }); diff --git a/common/utils.js b/common/utils.js new file mode 100644 index 0000000..ea206ef --- /dev/null +++ b/common/utils.js @@ -0,0 +1,92 @@ +const isNumeric = (value, errorMessage) => { + if (!/^\d+$/.test(value)) { + throw new Error(errorMessage); + } +}; + +const isBoolean = (value, errorMessage) => { + if (![true, false, 'true', 'false'].includes(value)) { + throw new Error(errorMessage); + } +}; + +const isZipCode = (value, errorMessage) => { + if (!/(^\d{5}$)|(^\d{5}-\d{4}$)/.test(value)) { + throw new Error(errorMessage); + } +}; + +const isAlphaNumeric = (value, errorMessage) => { + if (!/^[0-9a-zA-Z]+$/.test(value)) { + throw new Error(errorMessage); + } +}; + +const isPhoneNumber = (value, errorMessage) => { + if (!/^\d+$/.test(value) || value.length > 15) { + throw new Error(errorMessage); + } +}; + +// toCamel, isArray, and isObject are helper functions used within utils only +const toCamel = (s) => { + return s.replace(/([-_][a-z])/g, ($1) => { + return $1.toUpperCase().replace('-', '').replace('_', ''); + }); +}; + +const isArray = (a) => { + return Array.isArray(a); +}; + +const isISODate = (str) => { + try { + const ISOString = str.toISOString(); + if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(ISOString)) return false; + const d = new Date(ISOString); + return d.toISOString() === ISOString; + } catch (err) { + return false; + } +}; + +const isObject = (o) => { + return o === Object(o) && !isArray(o) && typeof o !== 'function' && !isISODate(o); +}; + +// Database columns are in snake case. JavaScript is suppose to be in camel case +// This function converts the keys from the sql query to camel case so it follows JavaScript conventions +const keysToCamel = (data) => { + if (isObject(data)) { + const newData = {}; + Object.keys(data).forEach((key) => { + newData[toCamel(key)] = keysToCamel(data[key]); + }); + return newData; + } + if (isArray(data)) { + return data.map((i) => { + return keysToCamel(i); + }); + } + if ( + typeof data === 'string' && + data.length > 0 && + data[0] === '{' && + data[data.length - 1] === '}' + ) { + let parsedList = data.replaceAll('"', ''); + parsedList = parsedList.slice(1, parsedList.length - 1).split(','); + return parsedList; + } + return data; +}; + +module.exports = { + isNumeric, + isBoolean, + isZipCode, + isAlphaNumeric, + isPhoneNumber, + keysToCamel, +}; diff --git a/routes/gameRouter.js b/routes/gameRouter.js new file mode 100644 index 0000000..7e26319 --- /dev/null +++ b/routes/gameRouter.js @@ -0,0 +1,45 @@ +const express = require('express'); +const { keysToCamel } = require('../common/utils'); +const { db } = require('../server/db'); + +const gameRouter = express.Router(); + +gameRouter.get('/rps/:memberId', async (req, res) => { + try { + const { memberId } = req.params + const [{move}] = await db.query(`SELECT move FROM rock_paper_scissors WHERE member_id = $1 LIMIT 1;`, [memberId]); + res.status(200).json({ memberId, move }); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + +gameRouter.get('/hangman/:memberId', async (req, res) => { + try { + const { memberId } = req.params + const [{phrase}] = await db.query(`SELECT phrase FROM hangman_phrases WHERE member_id = $1 LIMIT 1;`, [memberId]); + res.status(200).json({ memberId, phrase }); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + +gameRouter.get('/truthslies/:memberId', async (req, res) => { + try { + const { memberId } = req.params + const truths = await db.query(`SELECT truth FROM truths WHERE member_id = $1 LIMIT 2;`, [memberId]); + const [{lie}] = await db.query(`SELECT lie FROM lies WHERE member_id = $1 LIMIT 1;`, [memberId]); + res.status(200).json({ + truths: truths.map(({truth}) => (truth)), + lie + }); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + + +module.exports = gameRouter; diff --git a/routes/memberRouter.js b/routes/memberRouter.js new file mode 100644 index 0000000..3fd477c --- /dev/null +++ b/routes/memberRouter.js @@ -0,0 +1,40 @@ +const express = require('express'); +const { keysToCamel } = require('../common/utils'); +const { db } = require('../server/db'); + +const memberRouter = express.Router(); + +memberRouter.get('/', async (req, res) => { + try { + const basicUserInfo = await db.query(`SELECT id, member_name, member_year, project FROM members;`); + res.status(200).json(keysToCamel(basicUserInfo)); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + + +memberRouter.get('/ids', async (req, res) => { + try { + const ids = await db.query(`SELECT id FROM members;`); + res.status(200).json(ids.map(({id})=>(id))); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + +memberRouter.get('/:member_id', async (req, res) => { + try { + const { member_id } = req.params; + const memberInfo = await db.query(`SELECT * FROM members WHERE id = $1;`, [member_id]); + res.status(200).json(keysToCamel(memberInfo)); + } catch (err) { + console.log(err); + res.status(500).send(err.message); + } +}); + + +module.exports = memberRouter; diff --git a/server/schema/members.sql b/server/schema/members.sql index 3db3971..4f87447 100644 --- a/server/schema/members.sql +++ b/server/schema/members.sql @@ -2,8 +2,8 @@ DROP TABLE IF EXISTS members CASCADE; DROP TYPE IF EXISTS year_standing; DROP TYPE IF EXISTS ctc_project; -CREATE TYPE year_standing AS ENUM ('1st', '2nd', '3rd', "4th"); -CREATE TYPE ctc_project AS ENUM ('AISS', 'FPH', 'S2T', "(Board)"); +CREATE TYPE year_standing AS ENUM ('1st', '2nd', '3rd', '4th'); +CREATE TYPE ctc_project AS ENUM ('AISS', 'FPH', 'S2T', '(Board)'); CREATE TABLE members ( id SERIAL PRIMARY KEY, diff --git a/server/schema/truths_and_lies.sql b/server/schema/truths_and_lies.sql index 9ce32b6..cd122f0 100644 --- a/server/schema/truths_and_lies.sql +++ b/server/schema/truths_and_lies.sql @@ -16,5 +16,5 @@ CREATE TABLE truths ( CREATE TABLE lies ( member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE ON UPDATE CASCADE, - phrase TEXT NOT NULL + lie TEXT NOT NULL );