Files
erika/app/admin/form/[id]/result/export/route.ts
T

124 lines
3.5 KiB
TypeScript

import { NextResponse } from "next/server";
import { db } from "@/db";
import { answers } from "@/db/schema";
import { inArray } from "drizzle-orm";
import { requireAdmin } from "@/lib/auth/auth";
import {
getFilteredSubmissions,
parseResultFilters,
} from "@/lib/form/admin-form-results";
import { answerCellValue } from "@/lib/form/form-answer-values";
import { stripHtml } from "@/lib/utils";
function csvCell(value: unknown) {
const text = String(value ?? "");
return `"${text.replaceAll('"', '""')}"`;
}
function safeFilename(value: string) {
return (
value
.normalize("NFKD")
.replace(/[^\p{L}\p{N}]+/gu, "-")
.replace(/^-|-$/g, "")
.slice(0, 60) || "form"
);
}
function safeAsciiFilename(value: string) {
return (
value
.normalize("NFKD")
.replace(/[^\x00-\x7F]/g, "")
.replace(/[^A-Za-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 60) || "form"
);
}
function csvContentDisposition(title: string, date: string) {
const unicodeFilename = `${safeFilename(title)}-responses-${date}.csv`;
const asciiFilename = `${safeAsciiFilename(title)}-responses-${date}.csv`;
return `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodeURIComponent(unicodeFilename)}`;
}
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
await requireAdmin();
const { id } = await params;
const form = await db.query.forms.findFirst({
where: (record, { eq }) => eq(record.id, id),
});
if (!form) {
return NextResponse.json({ error: "Form not found" }, { status: 404 });
}
const url = new URL(request.url);
const filters = parseResultFilters(
Object.fromEntries(url.searchParams.entries())
);
const [{ rows }, questions] = await Promise.all([
getFilteredSubmissions(id, filters, { paginate: false }),
db.query.questions.findMany({
where: (question, { eq }) => eq(question.formId, id),
orderBy: (question, { asc }) => [asc(question.displayOrder)],
}),
]);
const answerRows =
rows.length === 0
? []
: await db
.select()
.from(answers)
.where(inArray(answers.submissionId, rows.map((row) => row.id)));
const answerMap = new Map(
answerRows.map((answer) => [
`${answer.submissionId}:${answer.questionId}`,
{ value: answer.value, imageCdnId: answer.imageCdnId ?? null },
])
);
const header = [
"Respondent",
"Discord ID",
"Submitted at (Asia/Bangkok)",
"Edit count",
...questions.map((question) => stripHtml(question.label)),
];
const lines = [
header.map(csvCell).join(","),
...rows.map((submission) =>
[
submission.userName || "Anonymous",
submission.userDiscordId || "",
submission.submittedAt.toLocaleString("en-GB", {
timeZone: "Asia/Bangkok",
hour12: false,
}),
Array.isArray(submission.editHistory)
? submission.editHistory.length
: 0,
...questions.map((question) =>
answerCellValue(
answerMap.get(`${submission.id}:${question.id}`)?.value || "",
question.type,
answerMap.get(`${submission.id}:${question.id}`)?.imageCdnId
)
),
]
.map(csvCell)
.join(",")
),
];
const date = new Date().toISOString().slice(0, 10);
return new NextResponse(`\uFEFF${lines.join("\r\n")}`, {
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": csvContentDisposition(form.title, date),
},
});
}