refactor(forms): isolate submission persistence
This commit is contained in:
@@ -45,6 +45,10 @@ export type FormFillerClientProps = {
|
||||
|
||||
export type FormValues = Record<string, string | string[] | undefined>;
|
||||
|
||||
export function formatQueuePosition(queueNumber: number) {
|
||||
return `#${queueNumber}`;
|
||||
}
|
||||
|
||||
export type StoredDraft = {
|
||||
version: 1;
|
||||
answers: FormValues;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { formatQueuePosition } from "@/lib/form/form-client";
|
||||
|
||||
describe("submission queue position", () => {
|
||||
test("keeps the database one-based position unchanged", () => {
|
||||
expect(formatQueuePosition(1)).toBe("#1");
|
||||
expect(formatQueuePosition(12)).toBe("#12");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import "server-only";
|
||||
|
||||
import { db } from "@/db";
|
||||
import { answers, submissions } from "@/db/schema";
|
||||
import { and, count, eq, lt, lte, or } from "drizzle-orm";
|
||||
import type {
|
||||
NormalizedSubmissionAnswer,
|
||||
StoredAnswerSnapshot,
|
||||
} from "@/lib/form/submission-intake";
|
||||
|
||||
export type SubmissionWriteResult = {
|
||||
submissionId: string;
|
||||
isUpdate: boolean;
|
||||
oldAnswersDict: Record<string, StoredAnswerSnapshot>;
|
||||
totalAnswers: number;
|
||||
queueNumber: number;
|
||||
};
|
||||
|
||||
export async function writeSubmission({
|
||||
formId,
|
||||
discordId,
|
||||
userName,
|
||||
allowMultipleSubmissions,
|
||||
normalizedAnswers,
|
||||
targetSubmissionId,
|
||||
}: {
|
||||
formId: string;
|
||||
discordId: string;
|
||||
userName: string | null;
|
||||
allowMultipleSubmissions: boolean;
|
||||
normalizedAnswers: NormalizedSubmissionAnswer[];
|
||||
targetSubmissionId?: string | null;
|
||||
}): Promise<SubmissionWriteResult> {
|
||||
return db.transaction(async (tx) => {
|
||||
const existingSubmission = allowMultipleSubmissions
|
||||
? targetSubmissionId
|
||||
? await tx.query.submissions.findFirst({
|
||||
where: (submission, { eq, and }) =>
|
||||
and(
|
||||
eq(submission.id, targetSubmissionId),
|
||||
eq(submission.formId, formId),
|
||||
eq(submission.userDiscordId, discordId),
|
||||
),
|
||||
})
|
||||
: null
|
||||
: await tx.query.submissions.findFirst({
|
||||
where: (submission, { eq, and }) =>
|
||||
and(eq(submission.formId, formId), eq(submission.userDiscordId, discordId)),
|
||||
});
|
||||
|
||||
if (allowMultipleSubmissions && targetSubmissionId && !existingSubmission) {
|
||||
throw new Error("ไม่พบคำตอบที่ต้องการแก้ไข");
|
||||
}
|
||||
|
||||
let submissionId = "";
|
||||
let submittedAt = new Date();
|
||||
const isUpdate = Boolean(existingSubmission);
|
||||
let oldAnswersDict: Record<string, StoredAnswerSnapshot> = {};
|
||||
|
||||
if (existingSubmission) {
|
||||
submissionId = existingSubmission.id;
|
||||
submittedAt = existingSubmission.submittedAt;
|
||||
|
||||
const oldAnswersList = await tx.query.answers.findMany({
|
||||
where: (answer, { eq }) => eq(answer.submissionId, submissionId),
|
||||
});
|
||||
|
||||
oldAnswersDict = oldAnswersList.reduce<Record<string, StoredAnswerSnapshot>>(
|
||||
(acc, curr) => {
|
||||
acc[curr.questionId] = {
|
||||
value: curr.value,
|
||||
imageCdnId: curr.imageCdnId ?? null,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const history = Array.isArray(existingSubmission.editHistory)
|
||||
? [...existingSubmission.editHistory]
|
||||
: [];
|
||||
history.push({
|
||||
editedAt: new Date().toISOString(),
|
||||
oldAnswers: oldAnswersDict,
|
||||
});
|
||||
|
||||
await tx
|
||||
.update(submissions)
|
||||
.set({ editHistory: history })
|
||||
.where(eq(submissions.id, submissionId));
|
||||
|
||||
await tx.delete(answers).where(eq(answers.submissionId, submissionId));
|
||||
} else {
|
||||
const [inserted] = await tx
|
||||
.insert(submissions)
|
||||
.values({ formId, userDiscordId: discordId, userName })
|
||||
.returning();
|
||||
submissionId = inserted.id;
|
||||
submittedAt = inserted.submittedAt;
|
||||
}
|
||||
|
||||
if (normalizedAnswers.length > 0) {
|
||||
await tx.insert(answers).values(
|
||||
normalizedAnswers.map((answer) => ({
|
||||
submissionId,
|
||||
questionId: answer.questionId,
|
||||
value: answer.value,
|
||||
imageCdnId: answer.imageCdnId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const [submissionStats] = await tx
|
||||
.select({ totalAnswers: count() })
|
||||
.from(submissions)
|
||||
.where(eq(submissions.formId, formId));
|
||||
const [queueStats] = await tx
|
||||
.select({ queueNumber: count() })
|
||||
.from(submissions)
|
||||
.where(
|
||||
and(
|
||||
eq(submissions.formId, formId),
|
||||
or(
|
||||
lt(submissions.submittedAt, submittedAt),
|
||||
and(
|
||||
eq(submissions.submittedAt, submittedAt),
|
||||
lte(submissions.id, submissionId),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
submissionId,
|
||||
isUpdate,
|
||||
oldAnswersDict,
|
||||
totalAnswers: submissionStats.totalAnswers,
|
||||
queueNumber: queueStats.queueNumber,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user