From 7562ad2a1659fdbc2b038b916eb03a22cc8deb59 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:44:19 +0530 Subject: [PATCH] refactor: Fix N+1 queries and optimize database operations (#25813) * refactor: prisma N+1 Queries * addressed review --- .../workflows/api/scheduleEmailReminders.ts | 48 +++++++++------- .../lib/repository/workflowReminder.ts | 25 +++++++++ .../eventtypes/lib/getEventTypesByViewer.ts | 38 ++++++------- .../viewer/eventTypes/heavy/update.handler.ts | 56 +++++++++++-------- .../viewer/workflows/update.handler.ts | 18 +++++- 5 files changed, 119 insertions(+), 66 deletions(-) diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 646837f87b..a906392085 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -47,7 +47,9 @@ export async function handler(req: NextRequest) { if (isSendgridEnabled) { const remindersToDelete: { referenceId: string | null; id: number }[] = await getAllRemindersToDelete(); + const reminderIds: number[] = []; const handlePastCancelledReminders = remindersToDelete.map(async (reminder) => { + reminderIds.push(reminder.id); try { if (reminder.referenceId) { await deleteScheduledSend(reminder.referenceId); @@ -55,19 +57,25 @@ export async function handler(req: NextRequest) { } catch (err) { logger.error(`Error deleting scheduled send (ref: ${reminder.referenceId}): ${err}`); } - - try { - await prisma.workflowReminder.update({ - where: { id: reminder.id }, - data: { referenceId: null }, - }); - } catch (err) { - logger.error(`Error updating reminder (id: ${reminder.id}): ${err}`); - } }); await Promise.allSettled(handlePastCancelledReminders); + if (reminderIds.length > 0) { + try { + await prisma.workflowReminder.updateMany({ + where: { + id: { + in: reminderIds, + }, + }, + data: { referenceId: null }, + }); + } catch (err) { + logger.error(`Error updating reminders: ${err}`); + } + } + //cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel(); @@ -89,12 +97,11 @@ export async function handler(req: NextRequest) { cancelUpdatePromises.push(cancelPromise, updatePromise); } - Promise.allSettled(cancelUpdatePromises).then((results) => { - results.forEach((result) => { - if (result.status === "rejected") { - logger.error(`Error cancelling scheduled_sends: ${result.reason}`); - } - }); + const results = await Promise.allSettled(cancelUpdatePromises); + results.forEach((result) => { + if (result.status === "rejected") { + logger.error(`Error cancelling scheduled_sends: ${result.reason}`); + } }); } @@ -490,12 +497,11 @@ export async function handler(req: NextRequest) { } } - Promise.allSettled(sendEmailPromises).then((results) => { - results.forEach((result) => { - if (result.status === "rejected") { - logger.error("Email sending failed", result.reason); - } - }); + const sendResults = await Promise.allSettled(sendEmailPromises); + sendResults.forEach((result) => { + if (result.status === "rejected") { + logger.error("Email sending failed", result.reason); + } }); return NextResponse.json({ message: `${unscheduledReminders.length} Emails to schedule` }, { status: 200 }); diff --git a/packages/features/ee/workflows/lib/repository/workflowReminder.ts b/packages/features/ee/workflows/lib/repository/workflowReminder.ts index 970bcd2141..1dd4cf2073 100644 --- a/packages/features/ee/workflows/lib/repository/workflowReminder.ts +++ b/packages/features/ee/workflows/lib/repository/workflowReminder.ts @@ -49,6 +49,31 @@ export class WorkflowReminderRepository { }); } + static async findWorkflowRemindersByStepIds(workflowStepIds: number[]) { + if (workflowStepIds.length === 0) { + return []; + } + + return await prisma.workflowReminder.findMany({ + where: { + workflowStepId: { + in: workflowStepIds, + }, + }, + select: { + id: true, + referenceId: true, + method: true, + workflowStepId: true, + booking: { + select: { + eventTypeId: true, + }, + }, + }, + }); + } + static async findWorkflowReminderForAIPhoneCallExecution(id: number) { const bookingSelect = { uid: true, diff --git a/packages/features/eventtypes/lib/getEventTypesByViewer.ts b/packages/features/eventtypes/lib/getEventTypesByViewer.ts index 7ad4096bd2..d387660d8b 100644 --- a/packages/features/eventtypes/lib/getEventTypesByViewer.ts +++ b/packages/features/eventtypes/lib/getEventTypesByViewer.ts @@ -118,31 +118,27 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo const mapEventType = async (eventType: UserEventTypes) => { const userRepo = new UserRepository(prisma); + const eventTypeUsers = eventType?.hosts?.length + ? eventType.hosts.map((host) => host.user) + : eventType.users; + const enrichedUsers = await userRepo.enrichUsersWithTheirProfiles(eventTypeUsers); + + const children = eventType.children || []; + const allChildUsers = children.flatMap((c) => c.users); + const enrichedAllChildUsers = await userRepo.enrichUsersWithTheirProfiles(allChildUsers); + const enrichedUsersMap = new Map(enrichedAllChildUsers.map((user) => [user.id, user])); + + const enrichedChildren = children.map((c) => ({ + ...c, + users: c.users.map((user) => enrichedUsersMap.get(user.id)).filter((user) => !!user), + })); + return { ...eventType, safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined, - users: await Promise.all( - (eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map( - async (u) => - await userRepo.enrichUserWithItsProfile({ - user: u, - }) - ) - ), + users: enrichedUsers, metadata: eventType.metadata ? eventTypeMetaDataSchemaWithUntypedApps.parse(eventType.metadata) : null, - children: await Promise.all( - (eventType.children || []).map(async (c) => ({ - ...c, - users: await Promise.all( - c.users.map( - async (u) => - await userRepo.enrichUserWithItsProfile({ - user: u, - }) - ) - ), - })) - ), + children: enrichedChildren, }; }; diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts index afa33909b6..ad07d171d0 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts @@ -430,34 +430,44 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }, }); - await Promise.all( - hostGroups.map(async (group) => { - await ctx.prisma.hostGroup.upsert({ - where: { id: group.id }, - update: { name: group.name }, - create: { + const existingGroupsMap = new Map(existingHostGroups.map((group) => [group.id, group])); + const newGroupsMap = new Map(hostGroups.map((group) => [group.id, group])); + + const groupsToCreate = hostGroups.filter((group) => !existingGroupsMap.has(group.id)); + const groupsToUpdate = hostGroups.filter((group) => existingGroupsMap.has(group.id)); + const groupsToDelete = existingHostGroups.filter((existingGroup) => !newGroupsMap.has(existingGroup.id)); + + await ctx.prisma.$transaction(async (tx) => { + // Create new groups + if (groupsToCreate.length > 0) { + await tx.hostGroup.createMany({ + data: groupsToCreate.map((group) => ({ id: group.id, name: group.name, eventTypeId: id, + })), + }); + } + + // Update existing groups + for (const group of groupsToUpdate) { + await tx.hostGroup.update({ + where: { id: group.id }, + data: { name: group.name }, + }); + } + + // Delete groups that are no longer in the new list + if (groupsToDelete.length > 0) { + await tx.hostGroup.deleteMany({ + where: { + id: { + in: groupsToDelete.map((group) => group.id), + }, }, }); - }) - ); - - const newGroupsMap = new Map(hostGroups.map((group) => [group.id, group])); - - // Delete groups that are no longer in the new list - const groupsToDelete = existingHostGroups.filter((existingGroup) => !newGroupsMap.has(existingGroup.id)); - - if (groupsToDelete.length > 0) { - await ctx.prisma.hostGroup.deleteMany({ - where: { - id: { - in: groupsToDelete.map((group) => group.id), - }, - }, - }); - } + } + }); } if (teamId && hosts) { diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 6058b1962e..dc0f6b6106 100755 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -253,6 +253,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const agentRepo = new PrismaAgentRepository(prisma); + const stepIds = userWorkflow.steps.map((step) => step.id); + const allReminders = await WorkflowReminderRepository.findWorkflowRemindersByStepIds(stepIds); + + const remindersByStepId = new Map(); + for (const reminder of allReminders) { + const stepId = reminder.workflowStepId; + if (stepId === null) continue; + + let list = remindersByStepId.get(stepId); + if (!list) { + list = []; + remindersByStepId.set(stepId, list); + } + list.push(reminder); + } + // handle deleted and edited workflow steps await Promise.all( userWorkflow.steps.map(async (oldStep) => { @@ -271,7 +287,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }; } - const remindersFromStep = await WorkflowReminderRepository.findWorkflowRemindersByStepId(oldStep.id); + const remindersFromStep = remindersByStepId.get(oldStep.id) || []; //step was deleted if (!newStep) { if (oldStep.action === WorkflowActions.CAL_AI_PHONE_CALL && !!oldStep.agentId) {