167 lines
4.2 KiB
TypeScript
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(),
|
|
}),
|
|
},
|
|
),
|
|
);
|