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), }, }); }