From d82f87aa2a8dcfdde649fe49628cc969e565b68b Mon Sep 17 00:00:00 2001 From: Volnei Munhoz Date: Tue, 21 Oct 2025 09:24:47 -0300 Subject: [PATCH] perf: Remove circular dependencies - wip (#24586) * wip * refactor(emails): remove circular dependencies from packages/emails - Extract renderEmail imports to use direct path (../src/renderEmail) instead of index - Create utility files for shared types and functions: - lib/utils/team-invite-utils.ts: TeamInvite type and utility functions - lib/utils/booking-redirect-types.ts: IBookingRedirect type - lib/utils/email-types.ts: OrganizationCreation and EmailVerifyCode types - lib/utils/date-formatting.ts: getFormattedDate utility function - Update all template files to import renderEmail directly from ../src/renderEmail - Update React template components to import from utility files instead of class templates - This eliminates all circular dependencies within packages/emails (reduced from 234 to 0 internal circular deps) Co-Authored-By: Volnei Munhoz * refactor(trpc): remove circular dependency in routing-forms - Extract ZFormByResponseIdInputSchema to separate schema file - Create getResponseWithFormFields.schema.ts to break circular dependency - Update imports in _router.ts and getResponseWithFormFields.handler.ts - Fix eslint warnings by updating deprecated rule names - This eliminates the circular dependency within packages/trpc (reduced from 1 to 0 internal circular deps) Co-Authored-By: Volnei Munhoz * fix(emails): correct TeamInvite type definition - Make isExistingUserMovedToOrg required (not optional) - Make prevLink and newLink non-optional (string | null) - Add back JSDoc comment for isAutoJoin field - This fixes type check errors in CI Co-Authored-By: Volnei Munhoz * wip * Fixes types folder --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../lib/types/booking-redirect-types.ts | 12 ++++ packages/emails/lib/types/email-types.ts | 23 +++++++ packages/emails/lib/utils/date-formatting.ts | 26 ++++++++ .../emails/lib/utils/team-invite-utils.ts | 59 +++++++++++++++++ .../src/templates/AttendeeRequestEmail.tsx | 4 +- .../BookingRedirectEmailNotification.tsx | 2 +- .../templates/OrganizationCreationEmail.tsx | 2 +- .../emails/src/templates/TeamInviteEmail.tsx | 2 +- .../src/templates/VerifyEmailByCode.tsx | 2 +- .../emails/templates/account-verify-email.ts | 2 +- .../admin-organization-notification.ts | 2 +- .../templates/attendee-add-guests-email.ts | 2 +- .../attendee-awaiting-payment-email.ts | 2 +- .../templates/attendee-cancelled-email.ts | 2 +- .../attendee-cancelled-seat-email.ts | 2 +- ...ee-daily-video-download-recording-email.ts | 2 +- ...e-daily-video-download-transcript-email.ts | 2 +- .../templates/attendee-declined-email.ts | 2 +- .../attendee-location-change-email.ts | 2 +- .../templates/attendee-request-email.ts | 2 +- .../templates/attendee-rescheduled-email.ts | 2 +- .../templates/attendee-scheduled-email.ts | 2 +- .../templates/attendee-updated-email.ts | 2 +- .../emails/templates/attendee-verify-email.ts | 15 +---- ...endee-was-requested-to-reschedule-email.ts | 2 +- .../booking-redirect-notification.ts | 16 +---- .../templates/broken-integration-email.ts | 2 +- .../templates/change-account-email-verify.ts | 2 +- .../credit-balance-limit-reached-email.ts | 2 +- .../credit-balance-low-warning-email.ts | 2 +- .../emails/templates/disabled-app-email.ts | 2 +- packages/emails/templates/feedback-email.ts | 2 +- .../emails/templates/forgot-password-email.ts | 2 +- .../emails/templates/monthly-digest-email.ts | 2 +- .../templates/no-show-fee-charged-email.ts | 2 +- .../emails/templates/org-auto-join-invite.ts | 2 +- .../templates/organization-creation-email.ts | 17 +---- .../templates/organizer-add-guests-email.ts | 2 +- ...organizer-attendee-cancelled-seat-email.ts | 2 +- .../templates/organizer-cancelled-email.ts | 2 +- ...er-daily-video-download-recording-email.ts | 2 +- ...r-daily-video-download-transcript-email.ts | 2 +- .../organizer-location-change-email.ts | 2 +- .../organizer-payment-refund-failed-email.ts | 2 +- .../templates/organizer-reassigned-email.ts | 2 +- .../templates/organizer-request-email.ts | 2 +- .../organizer-request-reminder-email.ts | 2 +- ...organizer-requested-to-reschedule-email.ts | 2 +- .../templates/organizer-rescheduled-email.ts | 2 +- .../templates/organizer-scheduled-email.ts | 2 +- .../templates/slug-replacement-email.ts | 2 +- .../emails/templates/team-invite-email.ts | 63 ++----------------- .../routers/apps/routing-forms/_router.ts | 9 +-- .../getResponseWithFormFields.handler.ts | 2 +- .../getResponseWithFormFields.schema.ts | 5 ++ packages/types/Calendar.d.ts | 8 +-- 56 files changed, 190 insertions(+), 155 deletions(-) create mode 100644 packages/emails/lib/types/booking-redirect-types.ts create mode 100644 packages/emails/lib/types/email-types.ts create mode 100644 packages/emails/lib/utils/date-formatting.ts create mode 100644 packages/emails/lib/utils/team-invite-utils.ts create mode 100644 packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.schema.ts diff --git a/packages/emails/lib/types/booking-redirect-types.ts b/packages/emails/lib/types/booking-redirect-types.ts new file mode 100644 index 0000000000..ffa136d0cf --- /dev/null +++ b/packages/emails/lib/types/booking-redirect-types.ts @@ -0,0 +1,12 @@ +import type { TFunction } from "i18next"; + +export interface IBookingRedirect { + language: TFunction; + fromEmail: string; + eventOwner: string; + toEmail: string; + toName: string; + oldDates?: string; + dates: string; + action: "add" | "update" | "cancel"; +} diff --git a/packages/emails/lib/types/email-types.ts b/packages/emails/lib/types/email-types.ts new file mode 100644 index 0000000000..f8e48dffaa --- /dev/null +++ b/packages/emails/lib/types/email-types.ts @@ -0,0 +1,23 @@ +import type { TFunction } from "i18next"; + +export type OrganizationCreation = { + language: TFunction; + from: string; + to: string; + ownerNewUsername: string; + ownerOldUsername: string | null; + orgDomain: string; + orgName: string; + prevLink: string | null; + newLink: string; +}; + +export type EmailVerifyCode = { + language: TFunction; + user: { + name?: string | null; + email: string; + }; + verificationEmailCode: string; + isVerifyingEmail?: boolean; +}; diff --git a/packages/emails/lib/utils/date-formatting.ts b/packages/emails/lib/utils/date-formatting.ts new file mode 100644 index 0000000000..3e6f1888cc --- /dev/null +++ b/packages/emails/lib/utils/date-formatting.ts @@ -0,0 +1,26 @@ +import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; +import type { CalendarEvent, Person } from "@calcom/types/Calendar"; + +export function getFormattedDate(calEvent: CalendarEvent, attendee: Person): string { + const inviteeTimeFormat = calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR; + const timezone = attendee.timeZone; + const locale = attendee.language.locale; + const t = attendee.language.translate; + + const getFormattedRecipientTime = (time: string, format: string) => { + return dayjs(time).tz(timezone).locale(locale).format(format); + }; + + const getInviteeStart = (format: string) => { + return getFormattedRecipientTime(calEvent.startTime, format); + }; + + const getInviteeEnd = (format: string) => { + return getFormattedRecipientTime(calEvent.endTime, format); + }; + + return `${getInviteeStart(inviteeTimeFormat)} - ${getInviteeEnd(inviteeTimeFormat)}, ${t( + getInviteeStart("dddd").toLowerCase() + )}, ${t(getInviteeStart("MMMM").toLowerCase())} ${getInviteeStart("D, YYYY")}`; +} diff --git a/packages/emails/lib/utils/team-invite-utils.ts b/packages/emails/lib/utils/team-invite-utils.ts new file mode 100644 index 0000000000..d9f8562cab --- /dev/null +++ b/packages/emails/lib/utils/team-invite-utils.ts @@ -0,0 +1,59 @@ +import type { TFunction } from "i18next"; + +import { APP_NAME } from "@calcom/lib/constants"; + +export type TeamInvite = { + language: TFunction; + from: string; + to: string; + teamName: string; + joinLink: string; + isCalcomMember: boolean; + /** + * We ideally should have a separate email for auto-join(when a user is automatically accepted into a team/org), but we don't have one yet. + */ + isAutoJoin: boolean; + isOrg: boolean; + parentTeamName: string | undefined; + isExistingUserMovedToOrg: boolean; + prevLink: string | null; + newLink: string | null; +}; + +export function getTypeOfInvite(teamInviteEvent: TeamInvite) { + if (teamInviteEvent.isOrg) { + return "TO_ORG"; + } + + if (teamInviteEvent.parentTeamName) { + return "TO_SUBTEAM"; + } + + if (teamInviteEvent.isAutoJoin) { + throw new Error("Auto-join is not supported for regular teams"); + } + + return "TO_REGULAR_TEAM"; +} + +export const getSubject = (teamInviteEvent: TeamInvite) => { + const typeOfInvite = getTypeOfInvite(teamInviteEvent); + const type = teamInviteEvent.isAutoJoin ? "added" : "invited"; + const variables = { + user: teamInviteEvent.from, + team: teamInviteEvent.teamName, + appName: APP_NAME, + parentTeamName: teamInviteEvent.parentTeamName, + entity: teamInviteEvent.language(teamInviteEvent.isOrg ? "organization" : "team").toLowerCase(), + }; + + if (typeOfInvite === "TO_ORG") { + return teamInviteEvent.language(`email_team_invite|subject|${type}_to_org`, variables); + } + + if (typeOfInvite === "TO_SUBTEAM") { + return teamInviteEvent.language(`email_team_invite|subject|${type}_to_subteam`, variables); + } + + return teamInviteEvent.language(`email_team_invite|subject|${type}_to_regular_team`, variables); +}; diff --git a/packages/emails/src/templates/AttendeeRequestEmail.tsx b/packages/emails/src/templates/AttendeeRequestEmail.tsx index 233a38c9bb..bf6071c81f 100644 --- a/packages/emails/src/templates/AttendeeRequestEmail.tsx +++ b/packages/emails/src/templates/AttendeeRequestEmail.tsx @@ -1,8 +1,8 @@ -import AttendeeScheduledEmailClass from "../../templates/attendee-rescheduled-email"; +import { getFormattedDate } from "../../lib/utils/date-formatting"; import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail"; export const AttendeeRequestEmail = (props: React.ComponentProps) => { - const date = new AttendeeScheduledEmailClass(props.calEvent, props.attendee).getFormattedDate(); + const date = getFormattedDate(props.calEvent, props.attendee); return ( { - const typeOfInvite = getTypeOfInvite(teamInviteEvent); - const type = teamInviteEvent.isAutoJoin ? "added" : "invited"; - const variables = { - user: teamInviteEvent.from, - team: teamInviteEvent.teamName, - appName: APP_NAME, - parentTeamName: teamInviteEvent.parentTeamName, - entity: teamInviteEvent.language(teamInviteEvent.isOrg ? "organization" : "team").toLowerCase(), - }; - - if (typeOfInvite === "TO_ORG") { - return teamInviteEvent.language(`email_team_invite|subject|${type}_to_org`, variables); - } - - if (typeOfInvite === "TO_SUBTEAM") { - return teamInviteEvent.language(`email_team_invite|subject|${type}_to_subteam`, variables); - } - - return teamInviteEvent.language(`email_team_invite|subject|${type}_to_regular_team`, variables); -}; +export { getSubject, getTypeOfInvite, type TeamInvite } from "../lib/utils/team-invite-utils"; export default class TeamInviteEmail extends BaseEmail { teamInviteEvent: TeamInvite; diff --git a/packages/trpc/server/routers/apps/routing-forms/_router.ts b/packages/trpc/server/routers/apps/routing-forms/_router.ts index 9f8e219ae3..9e4731bed4 100644 --- a/packages/trpc/server/routers/apps/routing-forms/_router.ts +++ b/packages/trpc/server/routers/apps/routing-forms/_router.ts @@ -8,10 +8,11 @@ import { ZFormMutationInputSchema } from "./formMutation.schema"; import { ZFormQueryInputSchema } from "./formQuery.schema"; import { ZGetAttributesForTeamInputSchema } from "./getAttributesForTeam.schema"; import { ZGetIncompleteBookingSettingsInputSchema } from "./getIncompleteBookingSettings.schema"; +import { ZFormByResponseIdInputSchema } from "./getResponseWithFormFields.schema"; import { forms } from "./procedures/forms"; import { ZSaveIncompleteBookingSettingsInputSchema } from "./saveIncompleteBookingSettings.schema"; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const UNSTABLE_HANDLER_CACHE: Record = {}; // TODO: Move getHandler and UNSTABLE_HANDLER_CACHE to a common utils file making sure that there is no name collision across routes @@ -24,7 +25,7 @@ const UNSTABLE_HANDLER_CACHE: Record = {}; */ const getHandler = async < T extends { - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type default: Function; } >( @@ -45,10 +46,6 @@ const getHandler = async < return UNSTABLE_HANDLER_CACHE[nameInCache] as unknown as T["default"]; }; -export const ZFormByResponseIdInputSchema = z.object({ - formResponseId: z.number(), -}); - export type TFormQueryInputSchema = z.infer; const appRoutingForms = router({ diff --git a/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.handler.ts b/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.handler.ts index 0359b2a2e5..d5e9e6ebfd 100644 --- a/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.handler.ts +++ b/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.handler.ts @@ -10,7 +10,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; -import type { ZFormByResponseIdInputSchema } from "./_router"; +import type { ZFormByResponseIdInputSchema } from "./getResponseWithFormFields.schema"; type GetResponseWithFormFieldsOptions = { ctx: { diff --git a/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.schema.ts b/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.schema.ts new file mode 100644 index 0000000000..0dd9b05ebb --- /dev/null +++ b/packages/trpc/server/routers/apps/routing-forms/getResponseWithFormFields.schema.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const ZFormByResponseIdInputSchema = z.object({ + formResponseId: z.number(), +}); diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index f4344dae23..2b7d9eda65 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -6,7 +6,6 @@ import type { Frequency } from "rrule"; import type z from "zod"; import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; -import type { Calendar } from "@calcom/features/calendars/weeklyview"; import type { TimeFormat } from "@calcom/lib/timeFormat"; import type { BookingSeat, @@ -66,7 +65,6 @@ export type EventBusyDetails = EventBusyDate & { userId?: number | null; }; -export type CalendarServiceType = typeof Calendar; export type AdditionalInfo = Record & { calWarnings?: string[] }; export type NewCalendarEventType = { @@ -104,9 +102,9 @@ export type CalendarEventType = { isNegative: boolean; }; organizer: string; - attendees: any[][]; + attendees: unknown[][]; recurrenceId: Time; - timezone: any; + timezone: string | object; }; export type BatchResponse = { @@ -314,7 +312,7 @@ export interface Calendar { /** * @see [How to inference class type that implements an interface](https://stackoverflow.com/a/64765554/6297100) */ -type Class = new (...args: Args) => I; +type Class = new (...args: Args) => I; export type CalendarClass = Class;