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:
co-authored by
Syed Ali Shahbaz
parent
cd06141816
commit
4497c4ce0b
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user