cal.pub0.org/packages/app-store/routing-forms/lib/getSerializableForm.ts

138 lines
4.4 KiB
TypeScript

import type { App_RoutingForms_Form } from "@prisma/client";
import type { z } from "zod";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import type { SerializableForm } from "../types/types";
import type { zodRoutesView, zodFieldsView } from "../zod";
import { zodFields, zodRoutes } from "../zod";
import getConnectedForms from "./getConnectedForms";
import isRouter from "./isRouter";
import isRouterLinkedField from "./isRouterLinkedField";
/**
* Doesn't have deleted fields by default
*/
export async function getSerializableForm<TForm extends App_RoutingForms_Form>(
form: TForm,
withDeletedFields = false
) {
const prisma = (await import("@calcom/prisma")).default;
const routesParsed = zodRoutes.safeParse(form.routes);
if (!routesParsed.success) {
throw new Error("Error parsing routes");
}
const fieldsParsed = zodFields.safeParse(form.fields);
if (!fieldsParsed.success) {
throw new Error("Error parsing fields" + fieldsParsed.error);
}
const settings = RoutingFormSettings.parse(
form.settings || {
// Would have really loved to do it using zod. But adding .default(true) throws type error in prisma/zod/app_routingforms_form.ts
emailOwnerOnSubmission: true,
}
);
const parsedFields =
(withDeletedFields ? fieldsParsed.data : fieldsParsed.data?.filter((f) => !f.deleted)) || [];
const parsedRoutes = routesParsed.data;
const fields = parsedFields as NonNullable<z.infer<typeof zodFieldsView>>;
const fieldsExistInForm: Record<string, true> = {};
parsedFields?.forEach((f) => {
fieldsExistInForm[f.id] = true;
});
const { routes, routers } = await getEnrichedRoutesAndRouters(parsedRoutes);
const connectedForms = (await getConnectedForms(prisma, form)).map((f) => ({
id: f.id,
name: f.name,
description: f.description,
}));
const finalFields = fields;
// Ideally we should't have needed to explicitly type it but due to some reason it's not working reliably with VSCode TypeCheck
const serializableForm: SerializableForm<TForm> = {
...form,
settings,
fields: finalFields,
routes,
routers,
connectedForms,
createdAt: form.createdAt.toString(),
updatedAt: form.updatedAt.toString(),
};
return serializableForm;
/**
* Enriches routes that are actually routers and returns a list of routers separately
*/
async function getEnrichedRoutesAndRouters(parsedRoutes: z.infer<typeof zodRoutes>) {
const routers: { name: string; description: string | null; id: string }[] = [];
const routes: z.infer<typeof zodRoutesView> = [];
if (!parsedRoutes) {
return { routes, routers };
}
for (const [, route] of Object.entries(parsedRoutes)) {
if (isRouter(route)) {
const router = await prisma.app_RoutingForms_Form.findFirst({
where: {
id: route.id,
userId: form.userId,
},
});
if (!router) {
throw new Error("Form -" + route.id + ", being used as router, not found");
}
const parsedRouter = await getSerializableForm(router, false);
routers.push({
name: parsedRouter.name,
description: parsedRouter.description,
id: parsedRouter.id,
});
// Enrichment
routes.push({
...route,
isRouter: true,
name: parsedRouter.name,
description: parsedRouter.description,
routes: parsedRouter.routes || [],
});
parsedRouter.fields?.forEach((field) => {
if (!fieldsExistInForm[field.id]) {
// Instead of throwing error, Log it instead of breaking entire routing forms feature
console.error(
"This is an impossible state. A router field must always be present in the connected form."
);
} else {
const currentFormField = fields.find((f) => f.id === field.id);
if (!currentFormField || !("routerId" in currentFormField)) {
return;
}
if (!isRouterLinkedField(field)) {
currentFormField.routerField = field;
}
currentFormField.router = {
id: parsedRouter.id,
name: router.name,
description: router.description || "",
};
}
});
} else {
routes.push(route);
}
}
return { routes, routers };
}
}