diff --git a/server/src/app.config.ts b/server/src/app.config.ts index b0f93b6..7767bcd 100644 --- a/server/src/app.config.ts +++ b/server/src/app.config.ts @@ -10,6 +10,7 @@ import { levelRepository } from "./levels/level.repository"; */ import { GameRoom } from "./rooms/GameRoom"; import { createLevelsRouter } from "./routes/levels"; +import { createQuestionsRouter } from "./routes/questions"; async function gracefulShutdown(signal: string) { console.log(`[Server] Received ${signal}. Closing MongoDB connection...`); @@ -36,6 +37,7 @@ export default config({ */ app.use(express.json({ limit: "1mb" })); app.use("/api", createLevelsRouter()); + app.use("/api", createQuestionsRouter()); app.get("/hello_world", (req, res) => { res.send("It's time to kick ass and chew bubblegum!"); diff --git a/server/src/questions/question.repository.ts b/server/src/questions/question.repository.ts new file mode 100644 index 0000000..542493e --- /dev/null +++ b/server/src/questions/question.repository.ts @@ -0,0 +1,81 @@ +import { randomUUID } from "crypto"; + +import { getMongoDb } from "../db/mongo"; +import { + CreateQuestionInput, + QuestionDocument, + UpdateQuestionInput, +} from "./question.types"; + +const QUESTIONS_COLLECTION = "questions"; + +export class QuestionRepository { + async listQuestions() { + const db = await getMongoDb(); + if (!db) return []; + + return db + .collection(QUESTIONS_COLLECTION) + .find({}) + .sort({ createdAt: -1 }) + .toArray(); + } + + async getById(id: string) { + const db = await getMongoDb(); + if (!db) return null; + + return db + .collection(QUESTIONS_COLLECTION) + .findOne({ id }); + } + + async createQuestion(input: CreateQuestionInput) { + const db = await getMongoDb(); + if (!db) throw new Error("MongoDB is not configured."); + + const now = new Date(); + const newQuestion: QuestionDocument = { + id: randomUUID(), + question: input.question, + createdAt: now, + updatedAt: now, + }; + + await db + .collection(QUESTIONS_COLLECTION) + .insertOne(newQuestion); + return newQuestion; + } + + async updateQuestion(id: string, input: UpdateQuestionInput) { + const db = await getMongoDb(); + if (!db) throw new Error("MongoDB is not configured."); + + const updatePayload: Partial = { + updatedAt: new Date(), + }; + + if (input.question) { + updatePayload.question = input.question; + } + + await db + .collection(QUESTIONS_COLLECTION) + .updateOne({ id }, { $set: updatePayload }); + + return this.getById(id); + } + + async deleteQuestion(id: string) { + const db = await getMongoDb(); + if (!db) throw new Error("MongoDB is not configured."); + + const result = await db + .collection(QUESTIONS_COLLECTION) + .deleteOne({ id }); + return result.deletedCount > 0; + } +} + +export const questionRepository = new QuestionRepository(); diff --git a/server/src/questions/question.types.ts b/server/src/questions/question.types.ts new file mode 100644 index 0000000..aa72f45 --- /dev/null +++ b/server/src/questions/question.types.ts @@ -0,0 +1,25 @@ +export type QuestionOption = { + text: string; + endResult?: string; + nextQuestion?: QuestionNode; +}; + +export type QuestionNode = { + title: string; + options: QuestionOption[]; +}; + +export type QuestionDocument = { + id: string; + question: QuestionNode; + createdAt: Date; + updatedAt: Date; +}; + +export type CreateQuestionInput = { + question: QuestionNode; +}; + +export type UpdateQuestionInput = { + question?: QuestionNode; +}; diff --git a/server/src/routes/questions.ts b/server/src/routes/questions.ts new file mode 100644 index 0000000..d544b78 --- /dev/null +++ b/server/src/routes/questions.ts @@ -0,0 +1,93 @@ +import { Request, Response, Router } from "express"; + +import { requireAdminAuth } from "../middleware/adminAuth"; +import { questionRepository } from "../questions/question.repository"; + +function idFromParams( + param: string | string[] | undefined, +): string | undefined { + return typeof param === "string" + ? param + : param !== undefined + ? param[0] + : undefined; +} + +export function createQuestionsRouter() { + const router = Router(); + + router.get("/questions", async (req: Request, res: Response) => { + const questions = await questionRepository.listQuestions(); + return res.json({ questions }); + }); + + router.get("/questions/:id", async (req: Request, res: Response) => { + const id = idFromParams(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid ID." }); + + const question = await questionRepository.getById(id); + if (!question) { + return res.status(404).json({ error: "Question not found." }); + } + return res.json({ question }); + }); + + router.post( + "/admin/questions", + requireAdminAuth, + async (req: Request, res: Response) => { + try { + const question = await questionRepository.createQuestion(req.body); + return res.status(201).json({ question }); + } catch (error: any) { + return res + .status(400) + .json({ error: error.message || "Unknown error." }); + } + }, + ); + + router.put( + "/admin/questions/:id", + requireAdminAuth, + async (req: Request, res: Response) => { + try { + const id = idFromParams(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid ID." }); + + const question = await questionRepository.updateQuestion(id, req.body); + if (!question) { + return res.status(404).json({ error: "Question not found." }); + } + return res.json({ question }); + } catch (error: any) { + return res + .status(400) + .json({ error: error.message || "Unknown error." }); + } + }, + ); + + router.delete( + "/admin/questions/:id", + requireAdminAuth, + async (req: Request, res: Response) => { + try { + const id = idFromParams(req.params.id); + if (!id) return res.status(400).json({ error: "Invalid ID." }); + + const success = await questionRepository.deleteQuestion(id); + if (!success) { + return res.status(404).json({ error: "Question not found." }); + } + return res.status(204).send(); + } catch (error: any) { + return res + .status(400) + .json({ error: error.message || "Unknown error." }); + } + }, + ); + + return router; +}