From 84408025ed38863a3b64f57d00eaf2da2f4b54f3 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 12 Sep 2023 10:12:25 -0300 Subject: [PATCH] perf: pre-load booking data for all users (#11243) * perf: pre-load booking data for all users * Fixing property issues * Fixes * Removed unwanted console.log * Made comment for intention of refactor more clear * fix: types * _count is optional * Assign to const bookings * Added mock for prisma booking * Fixed unit tests * Added lint ignore for prisma field * Update bookingScenario.ts * Fix linting --------- Co-authored-by: Alex van Andel --- apps/web/test/lib/getSchedule.test.ts | 3 +- apps/web/test/utils/bookingScenario.ts | 9 +- packages/core/getBusyTimes.ts | 95 +++++++++++-------- packages/core/getUserAvailability.ts | 12 ++- .../trpc/server/routers/viewer/slots/util.ts | 83 +++++++++++++++- 5 files changed, 159 insertions(+), 43 deletions(-) diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 7a7b5edf1b..8866232d1b 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -8,7 +8,7 @@ import prisma from "@calcom/prisma"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; -import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario"; +import { getDate, getGoogleCalendarCredential, createBookingScenario } from "../utils/bookingScenario"; // TODO: Mock properly prismaMock.eventType.findUnique.mockResolvedValue(null); @@ -1139,4 +1139,3 @@ describe("getSchedule", () => { }); }); }); - diff --git a/apps/web/test/utils/bookingScenario.ts b/apps/web/test/utils/bookingScenario.ts index 8b0f448e41..2dc3532257 100644 --- a/apps/web/test/utils/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario.ts @@ -194,10 +194,17 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const statusIn = where.OR[0].status?.in || []; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const userIdIn = where.OR[0].userId?.in || []; const firstConditionMatches = // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - statusIn.includes(booking.status) && booking.userId === where.OR[0].userId; + statusIn.includes(booking.status) && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (booking.userId === where.OR[0].userId || userIdIn.includes(booking.userId)); // We return this booking if either condition is met return firstConditionMatches; diff --git a/packages/core/getBusyTimes.ts b/packages/core/getBusyTimes.ts index b997e2bf8b..e9b8db1ce2 100644 --- a/packages/core/getBusyTimes.ts +++ b/packages/core/getBusyTimes.ts @@ -1,4 +1,4 @@ -import type { Credential } from "@prisma/client"; +import type { Booking, Credential, EventType } from "@prisma/client"; import { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; import dayjs from "@calcom/dayjs"; @@ -24,6 +24,17 @@ export async function getBusyTimes(params: { seatedEvent?: boolean; rescheduleUid?: string | null; duration?: number | null; + currentBookings?: + | (Pick & { + eventType: Pick< + EventType, + "id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot" + > | null; + _count?: { + seatsReferences: number; + }; + })[] + | null; }) { const { credentials, @@ -40,6 +51,7 @@ export async function getBusyTimes(params: { rescheduleUid, duration, } = params; + logger.silly( `Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({ userId, @@ -76,49 +88,56 @@ export async function getBusyTimes(params: { in: [BookingStatus.ACCEPTED], }, }; - // Find bookings that block this user from hosting further bookings. - const bookings = await prisma.booking.findMany({ - where: { - OR: [ - // User is primary host (individual events, or primary organizer) - { - ...sharedQuery, - userId, - }, - // The current user has a different booking at this time he/she attends - { - ...sharedQuery, - attendees: { - some: { - email: userEmail, + + // INFO: Refactored to allow this method to take in a list of current bookings for the user. + // Will keep support for retrieving a user's bookings if the caller does not already supply them. + // This function is called from multiple places but we aren't refactoring all of them at this moment + // to avoid potential side effects. + const bookings = params.currentBookings + ? params.currentBookings + : await prisma.booking.findMany({ + where: { + OR: [ + // User is primary host (individual events, or primary organizer) + { + ...sharedQuery, + userId, }, - }, + // The current user has a different booking at this time he/she attends + { + ...sharedQuery, + attendees: { + some: { + email: userEmail, + }, + }, + }, + ], }, - ], - }, - select: { - id: true, - uid: true, - startTime: true, - endTime: true, - title: true, - eventType: { select: { id: true, - afterEventBuffer: true, - beforeEventBuffer: true, - seatsPerTimeSlot: true, - }, - }, - ...(seatedEvent && { - _count: { - select: { - seatsReferences: true, + uid: true, + userId: true, + startTime: true, + endTime: true, + title: true, + eventType: { + select: { + id: true, + afterEventBuffer: true, + beforeEventBuffer: true, + seatsPerTimeSlot: true, + }, }, + ...(seatedEvent && { + _count: { + select: { + seatsReferences: true, + }, + }, + }), }, - }), - }, - }); + }); const bookingSeatCountMap: { [x: string]: number } = {}; const busyTimes = bookings.reduce( diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 9783478654..3675d9b2f0 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -1,4 +1,4 @@ -import type { Prisma } from "@prisma/client"; +import type { Booking, Prisma, EventType as PrismaEventType } from "@prisma/client"; import { z } from "zod"; import type { Dayjs } from "@calcom/dayjs"; @@ -128,6 +128,15 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni eventType?: EventType; currentSeats?: CurrentSeats; rescheduleUid?: string | null; + currentBookings?: (Pick & { + eventType: Pick< + PrismaEventType, + "id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot" + > | null; + _count?: { + seatsReferences: number; + }; + })[]; } ) { const { username, userId, dateFrom, dateTo, eventTypeId, afterEventBuffer, beforeEventBuffer, duration } = @@ -188,6 +197,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni seatedEvent: !!eventType?.seatsPerTimeSlot, rescheduleUid: initialData?.rescheduleUid || null, duration, + currentBookings: initialData?.currentBookings, }); const detailedBusyTimes: EventBusyDetails[] = [ diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 4db310ef62..08f1a1ceeb 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -15,6 +15,7 @@ import { performance } from "@calcom/lib/server/perfObserver"; import getSlots from "@calcom/lib/slots"; import prisma, { availabilityUserSelect } from "@calcom/prisma"; import { SchedulingType } from "@calcom/prisma/enums"; +import { BookingStatus } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { EventBusyDate } from "@calcom/types/Calendar"; @@ -291,6 +292,73 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) { usersWithCredentials = eventType.hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); } + const durationToUse = input.duration || 0; + + const startTimeDate = + input.rescheduleUid && durationToUse + ? startTime.subtract(durationToUse, "minute").toDate() + : startTime.toDate(); + + const endTimeDate = + input.rescheduleUid && durationToUse ? endTime.add(durationToUse, "minute").toDate() : endTime.toDate(); + + const sharedQuery = { + startTime: { gte: startTimeDate }, + endTime: { lte: endTimeDate }, + status: { + in: [BookingStatus.ACCEPTED], + }, + }; + + const currentBookingsAllUsers = await prisma.booking.findMany({ + where: { + OR: [ + // User is primary host (individual events, or primary organizer) + { + ...sharedQuery, + userId: { + in: usersWithCredentials.map((user) => user.id), + }, + }, + // The current user has a different booking at this time he/she attends + { + ...sharedQuery, + attendees: { + some: { + email: { + in: usersWithCredentials.map((user) => user.email), + }, + }, + }, + }, + ], + }, + select: { + id: true, + uid: true, + userId: true, + startTime: true, + endTime: true, + title: true, + attendees: true, + eventType: { + select: { + id: true, + afterEventBuffer: true, + beforeEventBuffer: true, + seatsPerTimeSlot: true, + }, + }, + ...(!!eventType?.seatsPerTimeSlot && { + _count: { + select: { + seatsReferences: true, + }, + }, + }), + }, + }); + /* We get all users working hours and busy slots */ const userAvailability = await Promise.all( usersWithCredentials.map(async (currentUser) => { @@ -310,7 +378,20 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) { beforeEventBuffer: eventType.beforeEventBuffer, duration: input.duration || 0, }, - { user: currentUser, eventType, currentSeats, rescheduleUid: input.rescheduleUid } + { + user: currentUser, + eventType, + currentSeats, + rescheduleUid: input.rescheduleUid, + currentBookings: currentBookingsAllUsers + .filter( + (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) + ) + .map((bookings) => { + const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; + return bookingWithoutAttendees; + }), + } ); if (!currentSeats && _currentSeats) currentSeats = _currentSeats; return {