import { Collection, Db, ObjectId } from 'mongodb'; import { checkNumberProp, checkObjectProp, checkStringProp } from './util.js'; /** * @type {Collection} */ let cGames; /** * @type {Collection} */ let cWalls; /** * @type {Collection} */ let cCategories; /** * @type {Collection} */ let cQuestions; const QuestionType = { SIMPLE: 'SIMPLE', MULTIPLE_CHOICE: 'MULTIPLE_CHOICE', IMAGE: 'IMAGE', IMAGE_MULTIPLE_CHOICE: 'IMAGE_MULTIPLE_CHOICE', AUDIO: 'AUDIO', AUDIO_MULTIPLE_CHOICE: 'AUDIO_MULTIPLE_CHOICE', }; /** * * @param {number} points * @param {string} question * @param {string} answer * @param {ObjectId} owner * @returns */ function createSimpleQuestion(points, question, answer, owner) { return { owner, points, type: QuestionType.SIMPLE, data: createSimpleData(question, answer), }; } /** * * @param {string} question * @param {string} answer * @returns */ function createSimpleData(question, answer) { return { question, answer, }; } /** * * @param {ObjectId[]} ids */ function splitQuestionIds(ids) { let res = []; for (let i = 0; i < ids.length; i += 5) { res.push(ids.slice(i, i + 5)); } return res; } /** * * @param {*} app * @param {Db} db */ export function initGames(app, db) { cGames = db.collection('games'); cWalls = db.collection('walls'); cCategories = db.collection('categories'); cQuestions = db.collection('questions'); app.get('/game', fetchGame); app.post('/game', createGame); app.delete('/game/:gameid', deleteGameRoute); app.get('/games', fetchGames); app.post('/game/rename', renameGame); app.get('/wall', fetchWall); app.get('/walls/:gameid', fetchWalls); app.post('/wall', createWall); app.delete('/wall/:wallid', deleteWallRoute); app.post('/wall/rename', renameWall); app.get('/category', fetchCategory); app.post('/category/rename', renameCategory); app.get('/question', fetchQuestion); app.post('/question', updateQuestion); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function createGame(req, res) { if (!checkStringProp(req.body, 'name')) { res.sendStatus(400); return; } const name = req.body.name; cGames .insertOne({ name, walls: [], owner: req.user._id, }) .then(() => { res.sendStatus(200); }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ function renameGame(req, res) { if (!checkStringProp(req.body, 'name')) { res.sendStatus(400); return; } if (!checkStringProp(req.body, 'gameid')) { res.sendStatus(400); return; } /** * @type {string} */ let gameid = req.body.gameid; let _id = new ObjectId(gameid); let name = req.body.name; cGames .updateOne( { _id, owner: req.user._id, }, { $set: { name, }, }, ) .then((result) => { if (result.modifiedCount === 1) res.sendStatus(200); else { console.error( `Failed to modify exactly one Document. Instead modified: ${result.modifiedCount}`, ); res.sendStatus(400); } }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchGames(req, res) { let list = cGames.find({ owner: req.user._id, }); res.status(200).send(await list.toArray()); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchGame(req, res) { if (req.query.id === undefined || req.query.id.length <= 0) { res.sendStatus(400); return; } const id = new ObjectId(req.query.id); let game = await cGames.findOne({ _id: id, owner: req.user._id, }); if (game) { res.status(200).send(game); } else { res.sendStatus(404); } } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function deleteGameRoute(req, res) { let game = await cGames.findOne({ owner: req.user._id, _id: new ObjectId(req.params.gameid), }); if (!game) { res.sendStatus(404); return; } deleteGame(game._id) .then(() => { res.sendStatus(200); }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchQuestion(req, res) { if (req.query.id === undefined || req.query.id.length <= 0) { res.sendStatus(400); return; } const id = new ObjectId(req.query.id); let question = await cQuestions.findOne({ _id: id, owner: req.user._id, }); if (question) { res.status(200).send(question); } else { res.sendStatus(500); } } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchCategory(req, res) { if (req.query.id === undefined || req.query.id.length <= 0) { res.sendStatus(400); return; } const id = new ObjectId(req.query.id); let category = await cCategories.findOne({ _id: id, owner: req.user._id, }); if (category) { let questions = await cQuestions .find( { _id: { $in: category.questions, }, owner: req.user._id, }, { projection: { _id: 1, points: 1, }, }, ) .toArray(); if (questions.length !== 5) { res.sendStatus(500); return; } category.questions = questions; res.status(200).send(category); } else { res.sendStatus(500); } } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ function renameCategory(req, res) { if (!checkStringProp(req.body, 'name')) { res.sendStatus(400); return; } if (!checkStringProp(req.body, 'categoryid')) { res.sendStatus(400); return; } /** * @type {string} */ let categoryid = req.body.categoryid; let _id = new ObjectId(categoryid); let name = req.body.name; cCategories .updateOne( { _id, owner: req.user._id, }, { $set: { name, }, }, ) .then((result) => { if (result.modifiedCount === 1) res.sendStatus(200); else { console.error( `Failed to modify exactly one Document. Instead modified: ${result.modifiedCount}`, ); res.sendStatus(400); } }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchWall(req, res) { if (req.query.id === undefined || req.query.id.length <= 0) { res.sendStatus(400); return; } const id = new ObjectId(req.query.id); let wall = await cWalls.findOne({ _id: id, owner: req.user._id, }); if (wall) { res.status(200).send(wall); } else { res.sendStatus(404); } } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function fetchWalls(req, res) { let game = await cGames.findOne({ owner: req.user._id, _id: new ObjectId(req.params.gameid), }); if (!game) { res.sendStatus(404); return; } let fetchedWalls = cWalls.find({ _id: { $in: game.walls, }, }); res.status(200).send(await fetchedWalls.toArray()); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ function renameWall(req, res) { if (!checkStringProp(req.body, 'name')) { res.sendStatus(400); return; } if (!checkStringProp(req.body, 'wallid')) { res.sendStatus(400); return; } /** * @type {string} */ let wallid = req.body.wallid; let _id = new ObjectId(wallid); let name = req.body.name; cWalls .updateOne( { _id, owner: req.user._id, }, { $set: { name, }, }, ) .then((result) => { if (result.modifiedCount === 1) res.sendStatus(200); else { console.error( `Failed to modify exactly one Document. Instead modified: ${result.modifiedCount}`, ); res.sendStatus(400); } }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function createWall(req, res) { if ( !checkStringProp(req.body, 'gameid') && !checkStringProp(req.body, 'name') ) { res.sendStatus(400); return; } /** * @type {string} */ const gameid = req.body.gameid; /** * @type {string} */ const wallname = req.body.name; let game = await cGames.findOne({ owner: req.user._id, _id: new ObjectId(gameid), }); if (!game) { res.sendStatus(404); return; } let newQuestions = []; for (let id = 1; id <= 5; id++) { for (let j = 1; j <= 5; j++) { newQuestions.push( createSimpleQuestion( j * -100, 'Frage', 'Antwort', req.user._id, ), ); } } cQuestions .insertMany(newQuestions) .then((insertedQuestions) => { let questionsIds = splitQuestionIds( Object.values(insertedQuestions.insertedIds), ); let newCategories = []; for (let i = 1; i <= 5; i++) { newCategories.push({ name: `Kategorie ${i}`, questions: questionsIds[i - 1], owner: req.user._id, }); } return cCategories.insertMany(newCategories); }) .then((insertedCategories) => { return cWalls.insertOne({ name: wallname, categories: Object.values(insertedCategories.insertedIds), owner: req.user._id, }); }) .then((insertedWall) => { return cGames.updateOne( { _id: game._id, }, { $push: { walls: insertedWall.insertedId, }, }, ); }) .then(() => { res.sendStatus(200); }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function deleteWallRoute(req, res) { let wall = await cWalls.findOne({ _id: new ObjectId(req.params.wallid), owner: req.user._id, }); if (!wall) { res.sendStatus(404); return; } let game = await cGames.findOne({ owner: req.user._id, walls: wall._id, }); if (!game) { res.sendStatus(404); return; } deleteWall(wall._id) .then(() => { return cGames.updateOne( { _id: game._id, }, { $pull: { walls: wall._id, }, }, ); }) .then(() => { res.sendStatus(200); }) .catch((err) => { console.error(err); res.sendStatus(500); }); } /** * * @param {import('express').Request} req * @param {import('express').Response} res */ async function updateQuestion(req, res) { if ( !checkStringProp(req.body, '_id') && !checkStringProp(req.body, 'owner') && !checkStringProp(req.body, 'type') && !checkNumberProp(req.body, 'points') && !checkObjectProp(req.body, 'data') ) { res.sendStatus(400); return; } if (req.body.owner !== req.user._id.toString()) { res.sendStatus(403); return; } /** * @type {string} */ let _id = req.body._id; let replacement; if ( req.body.type === QuestionType.SIMPLE || req.body.type === QuestionType.MULTIPLE_CHOICE ) { replacement = toNormalQuestion(req.body); } else if ( req.body.type === QuestionType.IMAGE || req.body.type === QuestionType.IMAGE_MULTIPLE_CHOICE ) { replacement = toImageQuestion(req.body); } else if ( req.body.type === QuestionType.AUDIO || req.body.type === QuestionType.AUDIO_MULTIPLE_CHOICE ) { replacement = toAudioQuestion(req.body); } cQuestions .replaceOne( { _id: new ObjectId(_id), owner: req.user._id, }, replacement, ) .then((result) => { if (result.modifiedCount === 1) { res.sendStatus(200); } else res.sendStatus(500); }) .catch((err) => { console.error(err); res.sendStatus(500); }); } function toNormalQuestion(body) { return { ...body, _id: new ObjectId(body._id), owner: new ObjectId(body.owner), }; } function toImageQuestion(body) { return { ...body, _id: new ObjectId(body._id), owner: new ObjectId(body.owner), data: { ...body.data, image: body.data.image === null ? null : new ObjectId(body.data.image), }, }; } function toAudioQuestion(body) { return { ...body, _id: new ObjectId(body._id), owner: new ObjectId(body.owner), data: { ...body.data, audio: body.data.audio === null ? null : new ObjectId(body.data.audio), }, }; } /** * * @param {ObjectId} _id */ function deleteGame(_id) { return cGames .findOne({ _id }) .then((game) => { let wallDeletions = []; for (const wallId of game.walls) { wallDeletions.push(deleteWall(wallId)); } return Promise.all(wallDeletions); }) .then(() => { return cGames.deleteOne({ _id }); }); } /** * * @param {ObjectId} _id */ function deleteWall(_id) { return cWalls .findOne({ _id }) .then((wall) => { let categoryDeletions = []; for (const catId of wall.categories) { categoryDeletions.push(deleteCategory(catId)); } return Promise.all(categoryDeletions); }) .then(() => { return cWalls.deleteOne({ _id: _id }); }); } /** * * @param {ObjectId} _id */ function deleteCategory(_id) { return cCategories .findOne({ _id, }) .then((category) => { return cQuestions.deleteMany({ _id: { $in: category.questions, }, }); }) .then(() => { return cCategories.deleteOne({ _id, }); }); }