Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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...`);
Expand All @@ -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!");
Expand Down
81 changes: 81 additions & 0 deletions server/src/questions/question.repository.ts
Original file line number Diff line number Diff line change
@@ -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<QuestionDocument>(QUESTIONS_COLLECTION)
.find({})
.sort({ createdAt: -1 })
.toArray();
}

async getById(id: string) {
const db = await getMongoDb();
if (!db) return null;

return db
.collection<QuestionDocument>(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<QuestionDocument>(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<QuestionDocument> = {
updatedAt: new Date(),
};

if (input.question) {
updatePayload.question = input.question;
}

await db
.collection<QuestionDocument>(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<QuestionDocument>(QUESTIONS_COLLECTION)
.deleteOne({ id });
return result.deletedCount > 0;
}
}

export const questionRepository = new QuestionRepository();
25 changes: 25 additions & 0 deletions server/src/questions/question.types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
93 changes: 93 additions & 0 deletions server/src/routes/questions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading