Skip to content

Commit 92eb41c

Browse files
authored
refactor: Migrate trpc routers in App store package to Trpc package (#23536)
* migrate trpc routers in app package to trpc package * delete * delete * delete * fix * fix * fix * format * format * fix * fix * fix * fix * remove unused file * fix * fix * wip
1 parent a1fca56 commit 92eb41c

32 files changed

Lines changed: 266 additions & 279 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import appBasecamp3 from "@calcom/app-store/basecamp3/trpc-router";
21
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
2+
import appBasecamp3 from "@calcom/trpc/server/routers/apps/basecamp3/_router";
33

44
export default createNextApiHandler(appBasecamp3);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import appRoutingForms from "@calcom/app-store/routing-forms/trpc-router";
21
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
2+
import appRoutingForms from "@calcom/trpc/server/routers/apps/routing-forms/_router";
33

44
export default createNextApiHandler(appRoutingForms);

packages/app-store/basecamp3/trpc-router.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/app-store/routing-forms/trpc/onFormSubmission.test.ts renamed to packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
66
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
77
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
88

9-
import { _onFormSubmission } from "./utils";
9+
import { _onFormSubmission } from "./formSubmissionUtils";
1010

1111
// Mock dependencies
1212
vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({

packages/app-store/routing-forms/lib/formSubmissionUtils.ts

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,244 @@
11
import type { Prisma } from "@prisma/client";
2+
import type { App_RoutingForms_Form, User } from "@prisma/client";
23

4+
import dayjs from "@calcom/dayjs";
5+
import type { Tasker } from "@calcom/features/tasker/tasker";
6+
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
7+
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
8+
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
9+
import logger from "@calcom/lib/logger";
10+
import { withReporting } from "@calcom/lib/sentryWrapper";
311
import { prisma } from "@calcom/prisma";
4-
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
12+
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
513
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
14+
import type { Ensure } from "@calcom/types/utils";
615

716
import { TRPCError } from "@trpc/server";
817

9-
import { onFormSubmission } from "../trpc/utils";
10-
import type { FormResponse, SerializableForm } from "../types/types";
18+
import type { FormResponse, SerializableForm, SerializableField, OrderedResponses } from "../types/types";
19+
20+
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/formSubmissionUtils"] });
21+
22+
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
23+
export type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
24+
string,
25+
{
26+
/**
27+
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
28+
*/
29+
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
30+
/**
31+
* @deprecated Use `response` instead
32+
*/
33+
value: FormResponse[keyof FormResponse]["value"];
34+
}
35+
>;
36+
37+
function isOptionsField(field: Pick<SerializableField, "type" | "options">) {
38+
return (field.type === "select" || field.type === "multiselect") && field.options;
39+
}
40+
41+
export function getFieldResponse({
42+
field,
43+
fieldResponseValue,
44+
}: {
45+
fieldResponseValue: FormResponse[keyof FormResponse]["value"];
46+
field: Pick<SerializableField, "type" | "options">;
47+
}) {
48+
if (!isOptionsField(field)) {
49+
return {
50+
value: fieldResponseValue,
51+
response: fieldResponseValue,
52+
};
53+
}
54+
55+
if (!field.options) {
56+
return {
57+
value: fieldResponseValue,
58+
response: fieldResponseValue,
59+
};
60+
}
61+
62+
const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue];
63+
64+
const chosenOptions = valueArray.map((idOrLabel) => {
65+
const foundOptionById = field.options?.find((option) => {
66+
return option.id === idOrLabel;
67+
});
68+
if (foundOptionById) {
69+
return {
70+
label: foundOptionById.label,
71+
id: foundOptionById.id,
72+
};
73+
} else {
74+
return {
75+
label: idOrLabel.toString(),
76+
id: null,
77+
};
78+
}
79+
});
80+
return {
81+
// value is a legacy prop that is just sending the labels which can change
82+
value: chosenOptions.map((option) => option.label),
83+
// response is new prop that is sending the label along with id(which doesn't change)
84+
response: chosenOptions,
85+
};
86+
}
87+
88+
export const sendResponseEmail = async (
89+
form: Pick<App_RoutingForms_Form, "id" | "name" | "fields">,
90+
orderedResponses: OrderedResponses,
91+
toAddresses: string[]
92+
) => {
93+
try {
94+
if (typeof window === "undefined") {
95+
const { default: ResponseEmail } = await import("../emails/templates/response-email");
96+
const email = new ResponseEmail({ form: form, toAddresses, orderedResponses });
97+
await email.sendEmail();
98+
}
99+
} catch (e) {
100+
moduleLogger.error("Error sending response email", e);
101+
}
102+
};
103+
104+
function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) {
105+
// If it's a team form, the target must be team webhook
106+
// If it's a user form, the target must be user webhook
107+
const isTeamForm = form.teamId;
108+
return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null };
109+
}
110+
111+
/**
112+
* Not called in preview mode or dry run mode
113+
* It takes care of sending webhooks and emails for form submissions
114+
*/
115+
export async function _onFormSubmission(
116+
form: Ensure<
117+
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
118+
"fields"
119+
>,
120+
response: FormResponse,
121+
responseId: number,
122+
chosenAction?: {
123+
type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl";
124+
value: string;
125+
}
126+
) {
127+
const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {};
128+
129+
for (const [fieldId, fieldResponse] of Object.entries(response)) {
130+
const field = form.fields.find((f) => f.id === fieldId);
131+
if (!field) {
132+
throw new Error(`Field with id ${fieldId} not found`);
133+
}
134+
// Use the label lowercased as the key to identify a field.
135+
// TODO: We seem to be using label from the response, Can we not use the field.label
136+
const key =
137+
form.fields.find((f) => f.id === fieldId)?.identifier ||
138+
(fieldResponse.label as keyof typeof fieldResponsesByIdentifier);
139+
fieldResponsesByIdentifier[key] = getFieldResponse({
140+
fieldResponseValue: fieldResponse.value,
141+
field,
142+
});
143+
}
144+
145+
const { userId, teamId } = getWebhookTargetEntity(form);
146+
147+
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
148+
149+
const subscriberOptionsFormSubmitted = {
150+
userId,
151+
teamId,
152+
orgId,
153+
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
154+
};
155+
156+
const subscriberOptionsFormSubmittedNoEvent = {
157+
userId,
158+
teamId,
159+
orgId,
160+
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
161+
};
162+
163+
const webhooksFormSubmitted = await getWebhooks(subscriberOptionsFormSubmitted);
164+
const webhooksFormSubmittedNoEvent = await getWebhooks(subscriberOptionsFormSubmittedNoEvent);
165+
166+
const promisesFormSubmitted = webhooksFormSubmitted.map((webhook) => {
167+
sendGenericWebhookPayload({
168+
secretKey: webhook.secret,
169+
triggerEvent: "FORM_SUBMITTED",
170+
createdAt: new Date().toISOString(),
171+
webhook,
172+
data: {
173+
formId: form.id,
174+
formName: form.name,
175+
teamId: form.teamId,
176+
responses: fieldResponsesByIdentifier,
177+
},
178+
rootData: {
179+
// Send responses unwrapped at root level for backwards compatibility
180+
...Object.entries(fieldResponsesByIdentifier).reduce((acc, [key, value]) => {
181+
acc[key] = value.value;
182+
return acc;
183+
}, {} as Record<string, FormResponse[keyof FormResponse]["value"]>),
184+
},
185+
}).catch((e) => {
186+
console.error(`Error executing routing form webhook`, webhook, e);
187+
});
188+
});
189+
190+
if (typeof window === "undefined") {
191+
try {
192+
const tasker: Tasker = await (await import("@calcom/features/tasker")).default;
193+
const promisesFormSubmittedNoEvent = webhooksFormSubmittedNoEvent.map((webhook) => {
194+
const scheduledAt = dayjs().add(15, "minute").toDate();
195+
196+
return tasker.create(
197+
"triggerFormSubmittedNoEventWebhook",
198+
{
199+
responseId,
200+
form: {
201+
id: form.id,
202+
name: form.name,
203+
teamId: form.teamId ?? null,
204+
},
205+
responses: fieldResponsesByIdentifier,
206+
redirect: chosenAction,
207+
webhook,
208+
},
209+
{ scheduledAt }
210+
);
211+
});
212+
213+
const promises = [...promisesFormSubmitted, ...promisesFormSubmittedNoEvent];
214+
215+
await Promise.all(promises);
216+
const orderedResponses = form.fields.reduce((acc, field) => {
217+
acc.push(response[field.id]);
218+
return acc;
219+
}, [] as OrderedResponses);
220+
221+
if (form.teamId) {
222+
if (form.userWithEmails?.length) {
223+
moduleLogger.debug(
224+
`Preparing to send Form Response email for Form:${form.id} to users: ${form.userWithEmails.join(
225+
","
226+
)}`
227+
);
228+
await sendResponseEmail(form, orderedResponses, form.userWithEmails);
229+
}
230+
} else if (form.settings?.emailOwnerOnSubmission) {
231+
moduleLogger.debug(
232+
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
233+
);
234+
await sendResponseEmail(form, orderedResponses, [form.user.email]);
235+
}
236+
} catch (e) {
237+
moduleLogger.error("Error triggering routing form response side effects", e);
238+
}
239+
}
240+
}
241+
export const onFormSubmission = withReporting(_onFormSubmission, "onFormSubmission");
11242

12243
export type TargetRoutingFormForResponse = SerializableForm<
13244
App_RoutingForms_Form & {

packages/app-store/routing-forms/trpc-router.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)