166 lines
4 KiB
TypeScript
166 lines
4 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, partyMember } from "../db/schema";
|
|
import { getMemberRecord } from "../party-data";
|
|
import type { QuizState } from "../party-types";
|
|
import { QuizWorkflow, quizQueue } from "../workflows/quiz";
|
|
import { pubsub } from "./party-socket";
|
|
|
|
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",
|
|
lastUpdated: new Date(),
|
|
})
|
|
.where(eq(party.id, params.partyId));
|
|
|
|
const members = await db
|
|
.select({
|
|
id: partyMember.id,
|
|
userId: partyMember.userId,
|
|
})
|
|
.from(partyMember)
|
|
.where(eq(partyMember.partyId, params.partyId));
|
|
|
|
pubsub.publish(
|
|
`party:${params.partyId}`,
|
|
JSON.stringify({
|
|
type: "party_status",
|
|
party: { status: "started" },
|
|
members,
|
|
}),
|
|
);
|
|
|
|
return {
|
|
message: "Quiz started",
|
|
workflowId: handle.workflowID,
|
|
};
|
|
},
|
|
{ auth: true },
|
|
)
|
|
.post(
|
|
"/response",
|
|
async ({ user, body, params, set }) => {
|
|
const existingQuiz = await db
|
|
.select({ data: party.data })
|
|
.from(party)
|
|
.where(eq(party.id, params.partyId))
|
|
.limit(1)
|
|
.then((rows) => rows[0]);
|
|
|
|
if (!existingQuiz) {
|
|
set.status = 404;
|
|
return { error: "Party not found" };
|
|
}
|
|
|
|
const quizData = (
|
|
(existingQuiz.data ?? {}) as Record<string, unknown>
|
|
).quiz as QuizState | undefined;
|
|
|
|
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",
|
|
);
|
|
|
|
const updatedParty = await db
|
|
.select({ data: party.data })
|
|
.from(party)
|
|
.where(eq(party.id, params.partyId))
|
|
.limit(1)
|
|
.then((rows) => rows[0]);
|
|
|
|
const updatedQuizData = (
|
|
(updatedParty?.data ?? {}) as Record<string, unknown>
|
|
).quiz as QuizState | undefined;
|
|
|
|
if (updatedQuizData) {
|
|
pubsub.publish(
|
|
`party:${params.partyId}`,
|
|
JSON.stringify({
|
|
type: "quiz_state",
|
|
quiz: updatedQuizData,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return { message: "Response recorded" };
|
|
},
|
|
{
|
|
auth: true,
|
|
body: t.Object({
|
|
selected: t.Integer(),
|
|
}),
|
|
},
|
|
)
|
|
.get(
|
|
"/status",
|
|
async ({ params, set }) => {
|
|
const existingQuiz = await db
|
|
.select({ data: party.data })
|
|
.from(party)
|
|
.where(eq(party.id, params.partyId))
|
|
.limit(1)
|
|
.then((rows) => rows[0]);
|
|
|
|
if (!existingQuiz) {
|
|
set.status = 404;
|
|
return { error: "Party not found" };
|
|
}
|
|
|
|
const quizData = (
|
|
(existingQuiz.data ?? {}) as Record<string, unknown>
|
|
).quiz as QuizState | undefined;
|
|
|
|
return {
|
|
quiz: quizData,
|
|
};
|
|
},
|
|
{ auth: true },
|
|
),
|
|
);
|