124 lines
3.5 KiB
TypeScript
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),
|
|
},
|
|
});
|
|
}
|