diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.test.ts index fc97951aa6..2e8c6174cd 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.test.ts @@ -120,6 +120,7 @@ describe("getBookings - PBAC Permission Checks", () => { findUnique: vi.fn(), groupBy: vi.fn(), }, + $queryRaw: vi.fn().mockResolvedValue([]), } as unknown as PrismaClient; // Create a comprehensive kysely mock that handles all chain methods @@ -338,7 +339,7 @@ describe("getBookings - PBAC Permission Checks", () => { it("should get event types from teams where user has booking.read permission", async () => { mockGetTeamIdsWithPermission.mockResolvedValue([1]); mockPrisma.user.findMany = vi.fn().mockResolvedValue([]); - mockPrisma.eventType.findMany = vi.fn().mockResolvedValue([{ id: 10 }, { id: 11 }]); + mockPrisma.$queryRaw = vi.fn().mockResolvedValue([{ id: 10 }, { id: 11 }]); mockPrisma.booking.groupBy = vi.fn().mockResolvedValue([]); await getBookings({ @@ -351,8 +352,8 @@ describe("getBookings - PBAC Permission Checks", () => { skip: 0, }); - // Verify event types are fetched from team 1 - expect(mockPrisma.eventType.findMany).toHaveBeenCalled(); + // Verify event types are fetched from team 1 using raw SQL query + expect(mockPrisma.$queryRaw).toHaveBeenCalled(); }); }); diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index df20225782..c565bafebc 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -1,7 +1,3 @@ -import type { Kysely } from "kysely"; -import { type SelectQueryBuilder } from "kysely"; -import { jsonObjectFrom, jsonArrayFrom } from "kysely/helpers/postgres"; - import dayjs from "@calcom/dayjs"; import getAllUserBookings from "@calcom/features/bookings/lib/getAllUserBookings"; import { isTextFilterValue } from "@calcom/features/data-table/lib/utils"; @@ -13,12 +9,13 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import type { PrismaClient } from "@calcom/prisma"; -import type { Booking, Prisma, Prisma as PrismaClientType } from "@calcom/prisma/client"; -import { SchedulingType, BookingStatus, MembershipRole } from "@calcom/prisma/enums"; +import type { Booking, Prisma as PrismaClientType } from "@calcom/prisma/client"; +import { Prisma } from "@calcom/prisma/client"; +import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; - import { TRPCError } from "@trpc/server"; - +import type { Kysely, SelectQueryBuilder } from "kysely"; +import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; import type { TrpcSessionUser } from "../../../types"; import type { TGetInputSchema } from "./get.schema"; @@ -440,7 +437,8 @@ export async function getBookings({ "Booking.metadata", "Booking.uid", eb - .cast( // Target TypeScript type + .cast( + // Target TypeScript type eb.ref("Booking.responses"), // Source column "jsonb" // Target SQL type ) @@ -784,7 +782,7 @@ type EnrichedUserData = { * @returns Bookings with attendees enriched with user data (name, email, avatarUrl, username) */ async function enrichAttendeesWithUserData< - TBooking extends { attendees: ReadonlyArray<{ id: number; email: string }> } + TBooking extends { attendees: ReadonlyArray<{ id: number; email: string }> }, >( bookings: TBooking[], kysely: Kysely @@ -833,38 +831,40 @@ async function enrichAttendeesWithUserData< })); } +/** + * Gets event type IDs for the given team IDs using an optimized raw SQL query. + * + * This query uses a UNION to combine: + * 1. Child event types whose parent belongs to the specified teams (managed event types) + * 2. Direct team event types that belong to the specified teams + * + * The subquery structure `WHERE "parent"."id" IN (SELECT "id" FROM "EventType" WHERE "teamId" IN (...)))` + * is intentional - it allows PostgreSQL to use the composite index on EventType(parentId, teamId) + * efficiently via Nested Loop joins, resulting in ~66x faster execution compared to a direct + * WHERE clause on parent.teamId (2.46ms vs 164ms in production benchmarks). + * + * @param prisma The Prisma client + * @param teamIds Array of team IDs to filter by + * @returns Array of event type IDs or undefined if no teamIds provided + */ async function getEventTypeIdsFromTeamIdsFilter(prisma: PrismaClient, teamIds?: number[]) { if (!teamIds || teamIds.length === 0) { return undefined; } - const [directTeamEventTypeIds, parentTeamEventTypeIds] = await Promise.all([ - prisma.eventType - .findMany({ - where: { - teamId: { in: teamIds }, - }, - select: { - id: true, - }, - }) - .then((eventTypes) => eventTypes.map((eventType) => eventType.id)), + const result = await prisma.$queryRaw<{ id: number }[]>` + SELECT "child"."id" + FROM "public"."EventType" AS "parent" + LEFT JOIN "public"."EventType" "child" ON ("parent"."id") = ("child"."parentId") + WHERE "parent"."id" IN (SELECT "id" FROM "public"."EventType" WHERE "teamId" IN (${Prisma.join(teamIds)})) + AND "child"."id" IS NOT NULL + UNION + SELECT "parent"."id" + FROM "public"."EventType" AS "parent" + WHERE "parent"."teamId" IN (${Prisma.join(teamIds)}) + `; - prisma.eventType - .findMany({ - where: { - parent: { - teamId: { in: teamIds }, - }, - }, - select: { - id: true, - }, - }) - .then((eventTypes) => eventTypes.map((eventType) => eventType.id)), - ]); - - return Array.from(new Set([...directTeamEventTypeIds, ...parentTeamEventTypeIds])); + return result.map((row) => row.id); } async function getAttendeeEmailsFromUserIdsFilter(