itpdp/api/src/routes/quiz.ts
Daniel Bulant d72bc9a303
restart
2026-05-16 15:11:38 +02:00

167 lines
4.2 KiB
TypeScript

import { DBOS } from "@dbos-inc/dbos-sdk";
import { eq } from "drizzle-orm";
import Elysia, { t } from "elysia";
import { betterAuthElysia } from "../auth";
import { db } from "../db";
import { party } from "../db/schema";
import { getMemberRecord, getPartyStatus } from "../party-data";
import type { QuizState } from "../party-types";
import { QuizWorkflow, quizQueue } from "../workflows/quiz";
import { pubsub } from "./party-socket";
function broadcastStatusToMembers(
status: Awaited<ReturnType<typeof getPartyStatus>>,
) {
if (!status) return;
const payload = JSON.stringify({
type: "party_status",
party: status.party,
members: status.members,
});
pubsub.publish(`party:${status.party.id}`, payload);
for (const member of status.members) {
pubsub.publish(`user:${member.userId}`, payload);
}
}
const quizWf = new QuizWorkflow();
export const quizRoutes = new Elysia()
.use(betterAuthElysia)
.group("/party/:partyId/quiz", (app) =>
app
.post(
"/start",
async ({ user, set, params }) => {
const membership = await getMemberRecord(db, user.id);
if (!membership || membership.partyId !== params.partyId) {
set.status = 403;
return { error: "Not a member of this party" };
}
const existingQuiz = await db
.select({ status: party.status })
.from(party)
.where(eq(party.id, params.partyId))
.limit(1)
.then((rows) => rows[0]);
if (existingQuiz?.status === "started") {
set.status = 409;
return { error: "Quiz already running" };
}
const handle = await DBOS.startWorkflow(quizWf.startQuiz, {
queueName: quizQueue.name,
enqueueOptions: { queuePartitionKey: params.partyId },
})(params.partyId);
await db
.update(party)
.set({
status: "started",
data: null,
lastUpdated: new Date(),
})
.where(eq(party.id, params.partyId));
const status = await getPartyStatus(params.partyId);
broadcastStatusToMembers(status);
return {
message: "Quiz started",
workflowId: handle.workflowID,
};
},
{ auth: true },
)
.post(
"/restart",
async ({ user, set, params }) => {
const membership = await getMemberRecord(db, user.id);
if (!membership || membership.partyId !== params.partyId) {
set.status = 403;
return { error: "Not a member of this party" };
}
const currentParty = await db.query.party.findFirst({
where: {
id: params.partyId,
},
});
if (!currentParty) {
set.status = 404;
return { error: "Party not found" };
}
const quizData = currentParty.data as QuizState | null;
if (!quizData || quizData.status !== "results") {
set.status = 409;
return { error: "Quiz is not finished yet" };
}
await db
.update(party)
.set({
status: "started",
data: null,
lastUpdated: new Date(),
})
.where(eq(party.id, params.partyId));
const handle = await DBOS.startWorkflow(quizWf.restartQuiz, {
queueName: quizQueue.name,
enqueueOptions: { queuePartitionKey: params.partyId },
})(params.partyId);
const status = await getPartyStatus(params.partyId);
broadcastStatusToMembers(status);
return {
message: "Quiz restarted",
workflowId: handle.workflowID,
};
},
{ auth: true },
)
.post(
"/response",
async ({ user, body, params, set }) => {
const party = await db.query.party.findFirst({
where: {
id: params.partyId,
},
});
if (!party) {
set.status = 404;
return { error: "Party not found" };
}
const quizData = party.data as QuizState | null;
if (!quizData || quizData.status !== "running") {
set.status = 400;
return { error: "Quiz not running" };
}
if (!quizData.workflowId) {
set.status = 500;
return { error: "Workflow ID not found" };
}
await DBOS.send(
quizData.workflowId,
{ playerId: user.id, selected: body.selected },
"quiz_responses",
);
return { message: "Response recorded" };
},
{
auth: true,
body: t.Object({
selected: t.Integer(),
}),
},
),
);