Skip to content

Commit

Permalink
Improve /router performance
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara committed Dec 24, 2024
1 parent 3c2619a commit 3dff0ca
Show file tree
Hide file tree
Showing 22 changed files with 779 additions and 580 deletions.
12 changes: 12 additions & 0 deletions packages/app-store/routing-forms/lib/getServerTimingHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function getServerTimingHeader(timeTaken: Record<string, number | null | undefined>) {
const headerParts = Object.entries(timeTaken)
.map(([key, value]) => {
if (value !== null && value !== undefined) {
return `${key};dur=${value}`;
}
return null;
})
.filter(Boolean);

return headerParts.join(", ");
}
200 changes: 200 additions & 0 deletions packages/app-store/routing-forms/lib/handleResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { Prisma } from "@prisma/client";
import { z } from "zod";

import { emailSchema } from "@calcom/lib/emailSchema";
import logger from "@calcom/lib/logger";
import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@calcom/trpc/server";

import isRouter from "../lib/isRouter";
import type { ZResponseInputSchema } from "../trpc/response.schema";
import { onFormSubmission } from "../trpc/utils";
import type { FormResponse, SerializableForm } from "../types/types";

const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/handleResponse"] });

export const handleResponse = async ({
response,
form,
// formFillerId,
chosenRouteId,
}: {
response: z.infer<typeof ZResponseInputSchema>["response"];
form: SerializableForm<
App_RoutingForms_Form & {
user: {
id: number;
email: string;
};
team: {
parentId: number | null;
} | null;
}
>;
formFillerId: string;
chosenRouteId: string | null;
}) => {
try {
if (!form.fields) {
// There is no point in submitting a form that doesn't have fields defined
throw new TRPCError({
code: "BAD_REQUEST",
});
}

const formTeamId = form.teamId;
const formOrgId = form.team?.parentId ?? null;
const serializableFormWithFields = {
...form,
fields: form.fields,
};

const missingFields = serializableFormWithFields.fields
.filter((field) => !(field.required ? response[field.id]?.value : true))
.map((f) => f.label);

if (missingFields.length) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Missing required fields ${missingFields.join(", ")}`,
});
}
const invalidFields = serializableFormWithFields.fields
.filter((field) => {
const fieldValue = response[field.id]?.value;
// The field isn't required at this point. Validate only if it's set
if (!fieldValue) {
return false;
}
let schema;
if (field.type === "email") {
schema = emailSchema;
} else if (field.type === "phone") {
schema = z.any();
} else {
schema = z.any();
}
return !schema.safeParse(fieldValue).success;
})
.map((f) => ({ label: f.label, type: f.type, value: response[f.id]?.value }));

if (invalidFields.length) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invalid value for fields ${invalidFields
.map((f) => `'${f.label}' with value '${f.value}' should be valid ${f.type}`)
.join(", ")}`,
});
}

const settings = RoutingFormSettings.parse(form.settings);
let userWithEmails: string[] = [];
if (form.teamId && settings?.sendUpdatesTo?.length) {
const userEmails = await prisma.membership.findMany({
where: {
teamId: form.teamId,
userId: {
in: settings.sendUpdatesTo,
},
},
select: {
user: {
select: {
email: true,
},
},
},
});
userWithEmails = userEmails.map((userEmail) => userEmail.user.email);
}

const chosenRoute = serializableFormWithFields.routes?.find((route) => route.id === chosenRouteId);
let teamMemberIdsMatchingAttributeLogic: number[] | null = null;
let timeTaken: Record<string, number | null> = {};
if (chosenRoute) {
if (isRouter(chosenRoute)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Chosen route is a router",
});
}

const teamMembersMatchingAttributeLogicWithResult =
formTeamId && formOrgId
? await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
response,
fields: form.fields || [],
},
attributesQueryValue: chosenRoute.attributesQueryValue ?? null,
fallbackAttributesQueryValue: chosenRoute.fallbackAttributesQueryValue,
teamId: formTeamId,
orgId: formOrgId,
},
{
enablePerf: true,
}
)
: null;

moduleLogger.debug(
"teamMembersMatchingAttributeLogic",
safeStringify({ teamMembersMatchingAttributeLogicWithResult })
);

teamMemberIdsMatchingAttributeLogic =
teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic
? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map(
(member) => member.userId
)
: null;

timeTaken = teamMembersMatchingAttributeLogicWithResult?.timeTaken ?? {};
} else {
// It currently happens for a Router route. Such a route id isn't present in the form.routes
}

const dbFormResponse = await prisma.app_RoutingForms_FormResponse.create({
data: {
// TODO: Why do we not save formFillerId available in the input?
// formFillerId,
formId: form.id,
response: response,
chosenRouteId,
},
});

await onFormSubmission(
{ ...serializableFormWithFields, userWithEmails },
dbFormResponse.response as FormResponse,
dbFormResponse.id,
chosenRoute ? ("action" in chosenRoute ? chosenRoute.action : undefined) : undefined
);

return {
isPreview: false,
formResponse: dbFormResponse,
teamMembersMatchingAttributeLogic: teamMemberIdsMatchingAttributeLogic,
attributeRoutingConfig: chosenRoute
? "attributeRoutingConfig" in chosenRoute
? chosenRoute.attributeRoutingConfig
: null
: null,
timeTaken,
};
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
throw new TRPCError({
code: "CONFLICT",
});
}
}
throw e;
}
};
75 changes: 46 additions & 29 deletions packages/app-store/routing-forms/pages/router/getServerSideProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData";
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "../../getEventTypeRedirectUrl";
import getFieldIdentifier from "../../lib/getFieldIdentifier";
import { getSerializableForm } from "../../lib/getSerializableForm";
import { getServerTimingHeader } from "../../lib/getServerTimingHeader";
import { handleResponse } from "../../lib/handleResponse";
import { findMatchingRoute } from "../../lib/processRoute";
import { substituteVariables } from "../../lib/substituteVariables";
import { getFieldResponseForJsonLogic } from "../../lib/transformResponse";
Expand All @@ -35,27 +37,8 @@ function getNamedParams(params: { appPages: string[] } | undefined) {
};
}

export const getServerSideProps = async function getServerSideProps(
context: AppGetServerSidePropsContext,
prisma: AppPrisma
) {
const queryParsed = querySchema.safeParse(context.query);
const { embed } = getNamedParams(context.params);
const pageProps = {
isEmbed: !!embed,
};

if (!queryParsed.success) {
log.warn("Error parsing query", queryParsed.error);
return {
notFound: true,
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
const { currentOrgDomain } = orgDomainConfig(context.req);

const form = await prisma.app_RoutingForms_Form.findFirst({
async function findFormById(formId: string, prisma: AppPrisma) {
return await prisma.app_RoutingForms_Form.findUnique({
where: {
id: formId,
},
Expand All @@ -64,6 +47,7 @@ export const getServerSideProps = async function getServerSideProps(
select: {
id: true,
username: true,
email: true,
movedToProfileId: true,
metadata: true,
organization: {
Expand All @@ -87,6 +71,33 @@ export const getServerSideProps = async function getServerSideProps(
},
},
});
}

export const getServerSideProps = async function getServerSideProps(
context: AppGetServerSidePropsContext,
prisma: AppPrisma
) {
const queryParsed = querySchema.safeParse(context.query);
const { embed } = getNamedParams(context.params);
const pageProps = {
isEmbed: !!embed,
};

if (!queryParsed.success) {
log.warn("Error parsing query", queryParsed.error);
return {
notFound: true,
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
const { currentOrgDomain } = orgDomainConfig(context.req);

let timeTaken: Record<string, number | null> = {};

const formQueryStart = performance.now();
const form = await findFormById(formId, prisma);
timeTaken.formQuery = performance.now() - formQueryStart;

if (!form) {
return {
Expand All @@ -95,20 +106,24 @@ export const getServerSideProps = async function getServerSideProps(
}

const { UserRepository } = await import("@calcom/lib/server/repository/user");
const profileEnrichmentStart = performance.now();
const formWithUserProfile = {
...form,
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
};
timeTaken.profileEnrichment = performance.now() - profileEnrichmentStart;

if (!(await isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain }))) {
if (!isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain })) {
return {
notFound: true,
};
}

const getSerializableFormStart = performance.now();
const serializableForm = await getSerializableForm({
form: enrichFormWithMigrationData(formWithUserProfile),
});
timeTaken.getSerializableForm = performance.now() - getSerializableFormStart;

const response: FormResponse = {};
if (!serializableForm.fields) {
Expand All @@ -131,25 +146,24 @@ export const getServerSideProps = async function getServerSideProps(

const decidedAction = matchingRoute.action;

const { createContext } = await import("@calcom/trpc/server/createContext");
const ctx = await createContext(context);

const { default: trpcRouter } = await import("@calcom/app-store/routing-forms/trpc/_router");
const caller = trpcRouter.createCaller(ctx);
const { v4: uuidv4 } = await import("uuid");
let teamMembersMatchingAttributeLogic = null;
let formResponseId = null;
let attributeRoutingConfig = null;
try {
const result = await caller.public.response({
formId: form.id,
const result = await handleResponse({
form: serializableForm,
formFillerId: uuidv4(),
response: response,
chosenRouteId: matchingRoute.id,
});
teamMembersMatchingAttributeLogic = result.teamMembersMatchingAttributeLogic;
formResponseId = result.formResponse.id;
attributeRoutingConfig = result.attributeRoutingConfig;
timeTaken = {
...timeTaken,
...result.timeTaken,
};
} catch (e) {
if (e instanceof TRPCError) {
return {
Expand All @@ -162,6 +176,9 @@ export const getServerSideProps = async function getServerSideProps(
}
}

// TODO: To be done using sentry tracing
console.log("Server-Timing", getServerTimingHeader(timeTaken));

//TODO: Maybe take action after successful mutation
if (decidedAction.type === "customPageMessage") {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppG
import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData";
import { getSerializableForm } from "../../lib/getSerializableForm";

export async function isAuthorizedToViewTheForm({
export function isAuthorizedToViewTheForm({
user,
currentOrgDomain,
}: {
Expand Down Expand Up @@ -108,7 +108,7 @@ export const getServerSideProps = async function getServerSideProps(
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
};

if (!(await isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain }))) {
if (!isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain })) {
return {
notFound: true,
};
Expand Down
Loading

0 comments on commit 3dff0ca

Please sign in to comment.