perf: improve get bookings performance (#16303)

* refactor: improve get bookings performance

* feat: use map to optimise

* chore: save

* chore: add authorization

* chore: remove condition

* chore: improvements

* chore

* chore: remove return

* chore: use Promise.all

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
Udit Takkar
2024-10-14 17:57:58 +05:30
committed by GitHub
co-authored by Syed Ali Shahbaz
parent cd06141816
commit 4497c4ce0b
3 changed files with 105 additions and 65 deletions
@@ -598,9 +598,7 @@ function BookingListItem(booking: BookingItemProps) {
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && (userId === booking.user?.id || booking.isUserTeamAdminOrOwner) && (
<TableActions actions={pendingActions} />
)}
{isPending && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
@@ -17,7 +17,12 @@ import { getTranslation } from "@calcom/lib/server";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import {
BookingStatus,
MembershipRole,
WebhookTriggerEvents,
UserPermissionRole,
} from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
@@ -77,7 +82,6 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
id: true,
name: true,
parentId: true,
members: true,
},
},
workflows: {
@@ -109,30 +113,13 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
},
});
if (booking.userId !== user.id && booking.eventTypeId) {
// Only query database when it is explicitly required.
const eventType = await prisma.eventType.findFirst({
where: {
id: booking.eventTypeId,
schedulingType: SchedulingType.COLLECTIVE,
},
select: {
users: {
select: {
id: true,
},
},
},
});
const membership = booking.eventType?.team?.members.find((membership) => membership.userId === user.id);
const isTeamAdminOrOwner =
membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN;
if (eventType && !eventType.users.find((user) => booking.userId === user.id) && !isTeamAdminOrOwner) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
}
}
await checkIfUserIsAuthorizedToConfirmBooking({
eventTypeId: booking.eventTypeId,
loggedInUserId: user.id,
teamId: booking.eventType?.teamId,
bookingUserId: booking.userId,
userRole: user.role,
});
// Do not move this before authorization check.
// This is done to avoid exposing extra information to the requester.
@@ -425,3 +412,61 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
return { message, status };
};
const checkIfUserIsAuthorizedToConfirmBooking = async ({
eventTypeId,
loggedInUserId,
teamId,
bookingUserId,
userRole,
}: {
eventTypeId: number | null;
loggedInUserId: number;
teamId?: number | null;
bookingUserId: number | null;
userRole: string;
}): Promise<void> => {
// check system wide admin
if (userRole === UserPermissionRole.ADMIN) return;
// Check if the user is the owner of the event type
if (bookingUserId === loggedInUserId) return;
// Check if user is associated with the event type
if (eventTypeId) {
const [loggedInUserAsHostOfEventType, loggedInUserAsUserOfEventType] = await Promise.all([
prisma.eventType.findUnique({
where: {
id: eventTypeId,
hosts: { some: { userId: loggedInUserId } },
},
select: { id: true },
}),
prisma.eventType.findUnique({
where: {
id: eventTypeId,
users: { some: { id: loggedInUserId } },
},
select: { id: true },
}),
]);
if (loggedInUserAsHostOfEventType || loggedInUserAsUserOfEventType) return;
}
// Check if the user is an admin/owner of the team the booking belongs to
if (teamId) {
const membership = await prisma.membership.findFirst({
where: {
userId: loggedInUserId,
teamId: teamId,
role: {
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
},
},
});
if (membership) return;
}
throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" });
};
@@ -3,7 +3,7 @@ import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings";
import type { PrismaClient } from "@calcom/prisma";
import { bookingMinimalSelect } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole, type BookingStatus } from "@calcom/prisma/enums";
import { type BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "../../../trpc";
@@ -229,7 +229,6 @@ export async function getBookings({
select: {
id: true,
name: true,
members: true,
},
},
},
@@ -427,41 +426,39 @@ export async function getBookings({
// Now enrich bookings with relation data. We could have queried the relation data along with the bookings, but that would cause unnecessary queries to the database.
// Because Prisma is also going to query the select relation data sequentially, we are fine querying it separately here as it would be just 1 query instead of 4
const bookings = (
await prisma.booking.findMany({
where: {
id: {
in: plainBookings.map((booking) => booking.id),
const bookings = await Promise.all(
(
await prisma.booking.findMany({
where: {
id: {
in: plainBookings.map((booking) => booking.id),
},
},
},
select: bookingSelect,
// We need to get the sorted bookings here as well because plainBookings array is not correctly sorted
orderBy,
select: bookingSelect,
// We need to get the sorted bookings here as well because plainBookings array is not correctly sorted
orderBy,
})
).map(async (booking) => {
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) {
booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email);
}
return {
...booking,
eventType: {
...booking.eventType,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
eventTypeColor: parseEventTypeColor(booking.eventType?.eventTypeColor),
price: booking.eventType?.price || 0,
currency: booking.eventType?.currency || "usd",
metadata: EventTypeMetaDataSchema.parse(booking.eventType?.metadata || {}),
},
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
};
})
).map((booking) => {
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) {
booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email);
}
const membership = booking.eventType?.team?.members.find((membership) => membership.userId === user.id);
const isUserTeamAdminOrOwner =
membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN;
return {
...booking,
eventType: {
...booking.eventType,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
eventTypeColor: parseEventTypeColor(booking.eventType?.eventTypeColor),
price: booking.eventType?.price || 0,
currency: booking.eventType?.currency || "usd",
metadata: EventTypeMetaDataSchema.parse(booking.eventType?.metadata || {}),
},
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
isUserTeamAdminOrOwner,
};
});
);
return { bookings, recurringInfo };
}