Skip to content

Commit 161ebdb

Browse files
authored
perf: Fix N+1 queries and optimize database operations (calcom#25630)
* perf: batch database operations and fix N+1 queries * delete * Fix condition for checking CalVideo location activity
1 parent 978c5bd commit 161ebdb

4 files changed

Lines changed: 114 additions & 69 deletions

File tree

packages/features/credentials/handleDeleteCredential.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -277,39 +277,47 @@ const handleDeleteCredential = async ({
277277
},
278278
});
279279

280-
for (const booking of unpaidBookings) {
281-
await prisma.booking.update({
282-
where: {
283-
id: booking.id,
280+
const unpaidBookingsIds = unpaidBookings.map((booking) => booking.id);
281+
const unpaidBookingsPaymentIds = unpaidBookings.flatMap((booking) =>
282+
booking.payment.map((payment) => payment.id)
283+
);
284+
await prisma.booking.updateMany({
285+
where: {
286+
id: {
287+
in: unpaidBookingsIds,
284288
},
285-
data: {
286-
status: BookingStatus.CANCELLED,
287-
cancellationReason: "Payment method removed",
289+
},
290+
data: {
291+
status: BookingStatus.CANCELLED,
292+
cancellationReason: "Payment method removed",
293+
},
294+
});
295+
for (const paymentId of unpaidBookingsPaymentIds) {
296+
await deletePayment(paymentId, credential);
297+
}
298+
await prisma.payment.deleteMany({
299+
where: {
300+
id: {
301+
in: unpaidBookingsPaymentIds,
288302
},
289-
});
290-
291-
for (const payment of booking.payment) {
292-
await deletePayment(payment.id, credential);
293-
await prisma.payment.delete({
294-
where: {
295-
id: payment.id,
296-
},
297-
});
298-
}
299-
300-
await prisma.attendee.deleteMany({
301-
where: {
302-
bookingId: booking.id,
303+
},
304+
});
305+
await prisma.attendee.deleteMany({
306+
where: {
307+
bookingId: {
308+
in: unpaidBookingsIds,
303309
},
304-
});
305-
306-
await prisma.bookingReference.updateMany({
307-
where: {
308-
bookingId: booking.id,
310+
},
311+
});
312+
await prisma.bookingReference.updateMany({
313+
where: {
314+
bookingId: {
315+
in: unpaidBookingsIds,
309316
},
310-
data: { deleted: true },
311-
});
312-
317+
},
318+
data: { deleted: true },
319+
});
320+
for (const booking of unpaidBookings) {
313321
const attendeesListPromises = booking.attendees.map(async (attendee) => {
314322
return {
315323
name: attendee.name,

packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ describe("createCRMEvent", () => {
131131
// Set up Prisma mocks with proper return values
132132
prismaMock.booking.findUnique.mockResolvedValueOnce(mockBooking);
133133
prismaMock.credential.findUnique.mockResolvedValueOnce(mockCredential);
134+
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
134135
prismaMock.bookingReference.createMany.mockResolvedValueOnce({ count: 1 });
135136
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
136137
const payload = JSON.stringify({
@@ -198,6 +199,7 @@ describe("createCRMEvent", () => {
198199
};
199200

200201
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
202+
prismaMock.credential.findMany.mockResolvedValueOnce([]);
201203

202204
const payload = JSON.stringify({
203205
bookingUid: "booking-123",
@@ -238,6 +240,7 @@ describe("createCRMEvent", () => {
238240

239241
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
240242
prismaMock.credential.findUnique.mockResolvedValue(null);
243+
prismaMock.credential.findMany.mockResolvedValueOnce([]);
241244
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
242245

243246
mockCreateEvent.mockRejectedValue(new Error("Salesforce API error"));
@@ -288,6 +291,7 @@ describe("createCRMEvent", () => {
288291

289292
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
290293
prismaMock.credential.findUnique.mockResolvedValue(mockCredential);
294+
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
291295
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
292296

293297
mockCreateEvent.mockRejectedValue(new RetryableError("Salesforce API Retryable error"));
@@ -334,6 +338,7 @@ describe("createCRMEvent", () => {
334338

335339
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
336340
prismaMock.credential.findUnique.mockResolvedValue(mockCredential);
341+
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
337342
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
338343

339344
mockCreateEvent.mockRejectedValue(new Error("Salesforce API error"));
@@ -401,6 +406,11 @@ describe("createCRMEvent", () => {
401406
.mockResolvedValueOnce(mockSalesforceCredential)
402407
.mockResolvedValueOnce(mockHubspotCredential);
403408

409+
prismaMock.credential.findMany.mockResolvedValueOnce([
410+
mockSalesforceCredential,
411+
mockHubspotCredential,
412+
]);
413+
404414
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
405415

406416
// Throw error for first app and resolve for second app
@@ -462,6 +472,7 @@ describe("createCRMEvent", () => {
462472

463473
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
464474
prismaMock.credential.findUnique.mockResolvedValueOnce(mockSalesforceCredential);
475+
prismaMock.credential.findMany.mockResolvedValueOnce([mockSalesforceCredential]);
465476
prismaMock.bookingReference.findMany.mockResolvedValueOnce([
466477
{ id: 1, type: "salesforce_crm", uid: "sf-event-123", credentialId: 1, bookingId: 1 },
467478
]);

packages/features/tasker/tasks/crm/createCRMEvent.ts

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -111,52 +111,72 @@ export async function createCRMEvent(payload: string): Promise<void> {
111111
});
112112

113113
const errorPerApp: Record<AppSlug, UnknownError> = {};
114-
// Find enabled CRM apps for the event type
114+
115+
// Parse apps and collect credential IDs for enabled CRM apps
116+
const appInfoMap = new Map<string, { app: any; credentialId: number }>();
117+
const credentialIds = new Set<number>();
118+
115119
for (const appSlug of Object.keys(eventTypeAppMetadata)) {
116-
// Try Catch per app to ensure all apps are tried even if any of them throws an error
117-
// If we want to retry for an error from this try catch, then that error must be thrown as a RetryableError
118-
try {
119-
const appData = eventTypeAppMetadata[appSlug as keyof typeof eventTypeAppMetadata];
120-
const appDataSchema = appDataSchemas[appSlug as keyof typeof appDataSchemas];
121-
if (!appData || !appDataSchema) {
122-
throw new Error(`Could not find appData or appDataSchema for ${appSlug}`);
123-
}
120+
const appData = eventTypeAppMetadata[appSlug as keyof typeof eventTypeAppMetadata];
121+
const appDataSchema = appDataSchemas[appSlug as keyof typeof appDataSchemas];
124122

125-
const appParse = appDataSchema.safeParse(appData);
123+
if (!appData || !appDataSchema) {
124+
throw new Error(`Could not find appData or appDataSchema for ${appSlug}`);
125+
}
126126

127-
if (!appParse.success) {
128-
log.error(`Error parsing event type app data for bookingUid ${bookingUid}`, appParse?.error);
129-
continue;
130-
}
127+
const appParse = appDataSchema.safeParse(appData);
131128

132-
const app = appParse.data;
133-
const hasCrmCategory =
134-
app.appCategories && app.appCategories.some((category: string) => category === "crm");
129+
if (!appParse.success) {
130+
log.error(`Error parsing event type app data for bookingUid ${bookingUid}`, appParse?.error);
131+
continue;
132+
}
135133

136-
if (!app.enabled || !app.credentialId || !hasCrmCategory) {
137-
log.info(`Skipping CRM app ${appSlug}`, {
138-
enabled: app.enabled,
139-
credentialId: app.credentialId,
140-
hasCrmCategory,
141-
});
142-
continue;
143-
}
134+
const app = appParse.data;
135+
const hasCrmCategory =
136+
app.appCategories && app.appCategories.some((category: string) => category === "crm");
144137

145-
const crmCredential = await prisma.credential.findUnique({
146-
where: {
147-
id: app.credentialId,
148-
},
149-
include: {
150-
user: {
151-
select: {
152-
email: true,
153-
},
154-
},
155-
},
138+
if (!app.enabled || !app.credentialId || !hasCrmCategory) {
139+
log.info(`Skipping CRM app ${appSlug}`, {
140+
enabled: app.enabled,
141+
credentialId: app.credentialId,
142+
hasCrmCategory,
156143
});
144+
continue;
145+
}
146+
147+
appInfoMap.set(appSlug, { app, credentialId: app.credentialId });
148+
credentialIds.add(app.credentialId);
149+
}
150+
151+
const crmCredentials = await prisma.credential.findMany({
152+
where: {
153+
id: {
154+
in: Array.from(credentialIds),
155+
},
156+
},
157+
include: {
158+
user: {
159+
select: {
160+
email: true,
161+
},
162+
},
163+
},
164+
});
165+
166+
const crmCredentialMap = new Map<number, (typeof crmCredentials)[number]>();
167+
for (const credential of crmCredentials) {
168+
crmCredentialMap.set(credential.id, credential);
169+
}
170+
//Find enabled CRM apps for the event type
171+
for (const appSlug of Array.from(appInfoMap.keys())) {
172+
const { app, credentialId } = appInfoMap.get(appSlug)!;
173+
// Try Catch per app to ensure all apps are tried even if any of them throws an error
174+
// If we want to retry for an error from this try catch, then that error must be thrown as a RetryableError
175+
try {
176+
const crmCredential = crmCredentialMap.get(credentialId);
157177

158178
if (!crmCredential) {
159-
throw new Error(`Credential not found for credentialId: ${app.credentialId}`);
179+
throw new Error(`Credential not found for credentialId: ${credentialId}`);
160180
}
161181

162182
const existingBookingReferenceForTheCredential = existingBookingReferences.find(

packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,19 @@ async function getTeamMembers({
7171
});
7272

7373
const userRepo = new UserRepository(prisma);
74+
const users = memberships.map((membership) => membership.user);
75+
const enrichedUsers = await userRepo.enrichUsersWithTheirProfileExcludingOrgMetadata(users);
76+
const enrichedUserMap = new Map<number, (typeof enrichedUsers)[0]>();
77+
enrichedUsers.forEach((enrichedUser) => {
78+
enrichedUserMap.set(enrichedUser.id, enrichedUser);
79+
});
7480
const membershipWithUserProfile = [];
7581
for (const membership of memberships) {
82+
const enrichedUser = enrichedUserMap.get(membership.user.id);
83+
if (!enrichedUser) continue;
7684
membershipWithUserProfile.push({
7785
...membership,
78-
user: await userRepo.enrichUserWithItsProfileExcludingOrgMetadata({
79-
user: membership.user,
80-
}),
86+
user: enrichedUser,
8187
});
8288
}
8389

0 commit comments

Comments
 (0)