Files
calendar/packages/app-store/routing-forms/api/responses/[formId].ts
T
Udit TakkarandGitHub 4e0e3a47f5 perf: routing forms loading time (#17518)
* perf: routing forms loading time

* chore: add TODO

* chore

* chore: add type

* chore: use Promise.all

* fix: type err

* fix: fetch id

* refactor: don't fetch team

* chore: add name
2024-11-12 17:32:42 +05:30

124 lines
3.8 KiB
TypeScript

import type { App_RoutingForms_Form } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { sanitizeValue } from "@calcom/lib/csvUtils";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import prisma from "@calcom/prisma";
import { getSerializableForm } from "../../lib/getSerializableForm";
import { ensureStringOrStringArray, getLabelsFromOptionIds } from "../../lib/reportingUtils";
import type { FormResponse, SerializableForm } from "../../types/types";
type Fields = NonNullable<SerializableForm<App_RoutingForms_Form>["fields"]>;
function getHumanReadableFieldResponseValue({
field,
value,
}: {
field: Fields[number];
value: string | number | string[];
}) {
if (field.options) {
const optionIds = ensureStringOrStringArray(value);
return getLabelsFromOptionIds({ options: field.options, optionIds });
} else {
return (value instanceof Array ? value : [value]).map(String);
}
}
async function* getResponses(formId: string, fields: Fields) {
let responses;
let skip = 0;
// Keep it small enough to be in Vercel limits of Serverless Function in terms of memory.
// To avoid limit in terms of execution time there is an RFC https://linear.app/calcom/issue/CAL-204/rfc-routing-form-improved-csv-exports
const take = 100;
while (
(responses = await prisma.app_RoutingForms_FormResponse.findMany({
where: {
formId,
},
take: take,
skip: skip,
})) &&
responses.length
) {
const csv: string[] = [];
responses.forEach((response) => {
const fieldResponses = response.response as FormResponse;
const csvCells: string[] = [];
fields.forEach((field) => {
const fieldResponse = fieldResponses[field.id];
const value = fieldResponse?.value || "";
const readableValues = getHumanReadableFieldResponseValue({ field, value });
const serializedValue = readableValues.map((value) => sanitizeValue(value)).join(" | ");
csvCells.push(serializedValue);
});
csvCells.push(response.createdAt.toISOString());
csv.push(csvCells.join(","));
});
skip += take;
yield csv.join("\n");
}
return "";
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { args } = req.query;
if (!args) {
throw new Error("args must be set");
}
const formId = args[2];
if (!formId) {
throw new Error("formId must be provided");
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Unauthorized" });
}
const { user } = session;
const form = await prisma.app_RoutingForms_Form.findFirst({
where: {
id: formId,
...entityPrismaWhereClause({ userId: user.id }),
},
});
if (!form) {
return res.status(404).json({ message: "Form not found or unauthorized" });
}
if (!(await canEditEntity(form, user.id))) {
return res.status(404).json({ message: "Form not found or unauthorized" });
}
const serializableForm = await getSerializableForm({ form, withDeletedFields: true });
res.setHeader("Content-Type", "text/csv; charset=UTF-8");
res.setHeader(
"Content-Disposition",
`attachment; filename="${serializableForm.name}-${serializableForm.id}.csv"`
);
res.setHeader("Transfer-Encoding", "chunked");
const headerFields = serializableForm.fields || [];
const csvIterator = getResponses(formId, headerFields);
// Make Header
res.write(
`${headerFields
.map((field) => `${field.label}${field.deleted ? "(Deleted)" : ""}`)
.concat(["Submission Time"])
.join(",")}\n`
);
for await (const partialCsv of csvIterator) {
res.write(partialCsv);
res.write("\n");
}
res.end();
}