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 <me@alexvanandel.com>
This commit is contained in:
Keith Williams 2023-09-12 10:12:25 -03:00 committed by GitHub
parent 9fba8c548b
commit 84408025ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 43 deletions

View File

@ -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", () => {
});
});
});

View File

@ -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;

View File

@ -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<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
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(

View File

@ -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<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
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[] = [

View File

@ -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 {