refactor(polls): centralize question semantics

This commit is contained in:
Simo committed 2026-09-01 22:15:44 +02:00
1 parent 66b606961b
commit abb04f8362
7 files changed
+228 -47

No files matched your search

+42 -22
View File
@@ -10,11 +10,17 @@ import {
WebsitePollVote,
} from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import {
type PollQuestionType,
parsePollAnswerSelections,
parsePollOptions,
} from "@/lib/polls/poll-semantics";
import { adminAction, authAction } from "@/lib/safe-action";
import { ActionError, actionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import {
createPollSchema,
pollQuestionPatchSchema,
pollQuestionSchema,
updatePollSchema,
voteOnPollSchema,
@@ -54,12 +60,19 @@ export const updatePoll = adminAction(
.select({
id: WebsitePoll.id,
title: WebsitePoll.title,
description: WebsitePoll.description,
status: WebsitePoll.status,
showResults: WebsitePoll.showResults,
multipleChoice: WebsitePoll.multipleChoice,
startsAt: WebsitePoll.startsAt,
endsAt: WebsitePoll.endsAt,
})
.from(WebsitePoll)
.where(eq(WebsitePoll.id, id))
.limit(1);
if (!existing) throw new ActionError("Poll not found");
const merged = createPollSchema.safeParse({ ...existing, ...data });
if (!merged.success) throw new ActionError("Invalid poll schedule");
await db
.update(WebsitePoll)
@@ -113,7 +126,7 @@ export const addPollQuestion = adminAction(
},
);
const updateQuestionInput = pollQuestionSchema.partial().extend({
const updateQuestionInput = pollQuestionPatchSchema.extend({
id: z.coerce.number().int().positive(),
});
@@ -121,6 +134,21 @@ export const updatePollQuestion = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: updateQuestionInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const [existing] = await db
.select({
pollId: WebsitePollQuestion.pollId,
question: WebsitePollQuestion.question,
type: WebsitePollQuestion.type,
sortOrder: WebsitePollQuestion.sortOrder,
options: WebsitePollQuestion.options,
})
.from(WebsitePollQuestion)
.where(eq(WebsitePollQuestion.id, id))
.limit(1);
if (!existing) throw new ActionError("Poll question not found");
const merged = pollQuestionSchema.safeParse({ ...existing, ...data });
if (!merged.success) throw new ActionError("Invalid poll question");
await db
.update(WebsitePollQuestion)
.set(data)
@@ -145,13 +173,6 @@ export const deletePollQuestion = adminAction(
// ── Public site: vote ───────────────────────────────────────────────
function parsePollOptions(options: string): string[] {
return options
.split("\n")
.map((o) => o.trim())
.filter(Boolean);
}
export const voteOnPoll = authAction(
{
schema: voteOnPollSchema,
@@ -215,26 +236,25 @@ export const voteOnPoll = authAction(
const answer = vote.answer.trim();
if (!answer) return actionError("Answer is required");
const options = parsePollOptions(question.options);
const selected = parsePollAnswerSelections(
question.type as PollQuestionType,
answer,
);
if (question.type === "text") {
if (answer.length > 500) {
return actionError("Answer is too long");
}
} else {
const options = parsePollOptions(question.options);
if (question.type === "multiple") {
const selected = answer
.split("\n")
.map((a) => a.trim())
.filter(Boolean);
if (selected.length === 0) {
return actionError("Select at least one option");
}
if (selected.some((a) => !options.includes(a))) {
return actionError("Invalid option selected");
}
} else if (!options.includes(answer)) {
} else if (question.type === "multiple") {
if (selected.length === 0) {
return actionError("Select at least one option");
}
if (selected.some((selection) => !options.includes(selection))) {
return actionError("Invalid option selected");
}
} else if (!options.includes(selected[0] ?? "")) {
return actionError("Invalid option selected");
}
const [existing] = await db