From 779c0142560a5b79273f5ee44ea95c995e0c2e56 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 6 May 2025 11:28:57 +0300 Subject: [PATCH] fix: get bookings advanced email and attendee filter (#21125) * fix: get bookings advanced email and attendee filter * bump libraries * bump libraries --- apps/api/v2/package.json | 2 +- .../routers/viewer/bookings/get.handler.ts | 144 ++++++++++-------- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index a754cc5774..c57d40b35d 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.194", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.196", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 4e8176f4e5..1121db9109 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -1,6 +1,6 @@ import type { Booking, Prisma, Prisma as PrismaClientType } from "@prisma/client"; import type { Kysely } from "kysely"; -import { DeduplicateJoinsPlugin, type SelectQueryBuilder } from "kysely"; +import { type SelectQueryBuilder } from "kysely"; import { jsonObjectFrom, jsonArrayFrom } from "kysely/helpers/postgres"; import dayjs from "@calcom/dayjs"; @@ -125,7 +125,7 @@ export async function getBookings({ getUserIdsAndEmailsWhereUserIsAdminOrOwner(prisma, membershipConditionWhereUserIsAdminOwner, user.orgId), ]); - const bookingQueries: BookingsUnionQuery[] = []; + const bookingQueries: { query: BookingsUnionQuery; tables: (keyof DB)[] }[] = []; // If user is organization owner/admin, contains organization members emails and ids (organization plan) // If user is only team owner/admin, contain team members emails and ids (teams plan) @@ -149,21 +149,22 @@ export async function getBookings({ } // 1. Booking created by one of the filtered users - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") .select("Booking.endTime") .select("Booking.createdAt") .select("Booking.updatedAt") - .where("userId", "in", filters.userIds) - ); + .where("userId", "in", filters.userIds), + tables: ["Booking"], + }); // 2. Attendee email matches one of the filtered users' emails if (attendeeEmailsFromUserIdsFilter?.length) { - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -171,14 +172,15 @@ export async function getBookings({ .select("Booking.createdAt") .select("Booking.updatedAt") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") - .where("Attendee.email", "in", attendeeEmailsFromUserIdsFilter) - ); + .where("Attendee.email", "in", attendeeEmailsFromUserIdsFilter), + tables: ["Booking", "Attendee"], + }); } // 3. Seat reference attendee email matches one of the filtered users' emails if (attendeeEmailsFromUserIdsFilter?.length) { - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -187,24 +189,26 @@ export async function getBookings({ .select("Booking.updatedAt") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") .innerJoin("BookingSeat", "Attendee.id", "BookingSeat.attendeeId") - .where("Attendee.email", "in", attendeeEmailsFromUserIdsFilter) - ); + .where("Attendee.email", "in", attendeeEmailsFromUserIdsFilter), + tables: ["Booking", "Attendee", "BookingSeat"], + }); } } else { // 1. Current user created bookings - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") .select("Booking.endTime") .select("Booking.createdAt") .select("Booking.updatedAt") - .where("Booking.userId", "=", user.id) - ); + .where("Booking.userId", "=", user.id), + tables: ["Booking"], + }); // 2. Current user is an attendee - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -212,11 +216,12 @@ export async function getBookings({ .select("Booking.createdAt") .select("Booking.updatedAt") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") - .where("Attendee.email", "=", user.email) - ); + .where("Attendee.email", "=", user.email), + tables: ["Booking", "Attendee"], + }); // 3. Current user is an attendee via seats reference - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -225,14 +230,15 @@ export async function getBookings({ .select("Booking.updatedAt") .innerJoin("BookingSeat", "BookingSeat.bookingId", "Booking.id") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") - .where("Attendee.email", "=", user.email) - ); + .where("Attendee.email", "=", user.email), + tables: ["Booking", "Attendee", "BookingSeat"], + }); // 4. Scope depends on `user.orgId`: // - If Current user is ORG_OWNER/ADMIN so we get bookings where organization members are attendees // - If Current user is TEAM_OWNER/ADMIN so we get bookings where team members are attendees userEmailsWhereUserIsAdminOrOwner?.length && - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -240,14 +246,15 @@ export async function getBookings({ .select("Booking.createdAt") .select("Booking.updatedAt") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") - .where("Attendee.email", "in", userEmailsWhereUserIsAdminOrOwner) - ); + .where("Attendee.email", "in", userEmailsWhereUserIsAdminOrOwner), + tables: ["Booking", "Attendee"], + }); // 5. Scope depends on `user.orgId`: // - If Current user is ORG_OWNER/ADMIN so we get bookings where organization members are attendees via seatsReference // - If Current user is TEAM_OWNER/ADMIN so we get bookings where team members are attendees via seatsReference userEmailsWhereUserIsAdminOrOwner?.length && - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -256,15 +263,16 @@ export async function getBookings({ .select("Booking.updatedAt") .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") .innerJoin("BookingSeat", "Attendee.id", "BookingSeat.attendeeId") - .where("Attendee.email", "in", userEmailsWhereUserIsAdminOrOwner) - ); + .where("Attendee.email", "in", userEmailsWhereUserIsAdminOrOwner), + tables: ["Booking", "Attendee", "BookingSeat"], + }); // 6. Scope depends on `user.orgId`: // - If Current user is ORG_OWNER/ADMIN, get booking created for an event type within the organization // - If Current user is TEAM_OWNER/ADMIN, get bookings created for an event type within the team eventTypeIdsWhereUserIsAdminOrOwner?.length && - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") @@ -272,26 +280,28 @@ export async function getBookings({ .select("Booking.createdAt") .select("Booking.updatedAt") .innerJoin("EventType", "EventType.id", "Booking.eventTypeId") - .where("Booking.eventTypeId", "in", eventTypeIdsWhereUserIsAdminOrOwner) - ); + .where("Booking.eventTypeId", "in", eventTypeIdsWhereUserIsAdminOrOwner), + tables: ["Booking", "EventType"], + }); // 7. Scope depends on `user.orgId`: // - If Current user is ORG_OWNER/ADMIN, get bookings created by users within the same organization // - If Current user is TEAM_OWNER/ADMIN, get bookings created by users within the same organization userIdsWhereUserIsAdminOrOwner?.length && - bookingQueries.push( - kysely + bookingQueries.push({ + query: kysely .selectFrom("Booking") .select("Booking.id") .select("Booking.startTime") .select("Booking.endTime") .select("Booking.createdAt") .select("Booking.updatedAt") - .where("Booking.userId", "in", userIdsWhereUserIsAdminOrOwner) - ); + .where("Booking.userId", "in", userIdsWhereUserIsAdminOrOwner), + tables: ["Booking"], + }); } - const queriesWithFilters = bookingQueries.map((query) => { + const queriesWithFilters = bookingQueries.map(({ query, tables }) => { // 1. Apply mandatory status filter let fullQuery = addStatusesQueryFilters(query, bookingListingByStatus); @@ -319,7 +329,8 @@ export async function getBookings({ fullQuery, "name", filters.attendeeName.data.operator, - filters.attendeeName.data.operand + filters.attendeeName.data.operand, + tables.includes("Attendee") ); } } @@ -329,16 +340,16 @@ export async function getBookings({ if (typeof filters.attendeeEmail === "string") { // Simple string match (exact) fullQuery = fullQuery - .withPlugin(new DeduplicateJoinsPlugin()) .innerJoin("Attendee", "Attendee.bookingId", "Booking.id") .where("Attendee.email", "=", filters.attendeeEmail.trim()); } else if (isTextFilterValue(filters.attendeeEmail)) { // TODO: write makeWhereClause equivalent for kysely fullQuery = addAdvancedAttendeeWhereClause( - fullQuery.withPlugin(new DeduplicateJoinsPlugin()), + fullQuery, "email", filters.attendeeEmail.data.operator, - filters.attendeeEmail.data.operand + filters.attendeeEmail.data.operand, + tables.includes("Attendee") ); } } @@ -383,6 +394,7 @@ export async function getBookings({ const bookingsFromUnion = (await kysely.executeQuery(getBookingsUnionCompiled)).rows; log.debug(`Get bookings for user ${user.id} SQL:`, getBookingsUnionCompiled.sql); + const totalCount = Number( ( await kysely @@ -912,53 +924,55 @@ function addAdvancedAttendeeWhereClause( | "notContains" | "isEmpty" | "isNotEmpty", - operand: string + operand: string, + isAttendeeTableJoined: boolean ) { + let fullQuery = query.$if(!isAttendeeTableJoined, (eb) => + eb.innerJoin("Attendee", "Attendee.id", "Booking.id") + ) as SelectQueryBuilder< + DB, + "Booking" | "Attendee", + Pick + >; + switch (operator) { case "endsWith": - query - .innerJoin("Attendee", "Attendee.id", "Booking.id") - .where(`Attendee.${key}`, "like", `%${operand}`); + fullQuery = fullQuery.where(`Attendee.${key}`, "like", `%${operand}`); break; + case "startsWith": - query - .innerJoin("Attendee", "Attendee.id", "Booking.id") - .where(`Attendee.${key}`, "like", `${operand}%`); + fullQuery = fullQuery.where(`Attendee.${key}`, "like", `${operand}%`); break; case "equals": - query.innerJoin("Attendee", "Attendee.id", "Booking.id").where(`Attendee.${key}`, "=", `${operand}`); + fullQuery = fullQuery.where(`Attendee.${key}`, "=", `${operand}`); break; case "notEquals": - query.innerJoin("Attendee", "Attendee.id", "Booking.id").where(`Attendee.${key}`, "!=", `${operand}`); + fullQuery = fullQuery.where(`Attendee.${key}`, "!=", `${operand}`); break; case "contains": - query - .innerJoin("Attendee", "Attendee.id", "Booking.id") - .where(`Attendee.${key}`, "like", `%${operand}%`); + fullQuery = fullQuery.where(`Attendee.${key}`, "like", `%${operand}%`); break; case "notContains": - query - .innerJoin("Attendee", "Attendee.id", "Booking.id") - .where(`Attendee.${key}`, "not like", `%${operand}%`); + fullQuery = fullQuery.where(`Attendee.${key}`, "not like", `%${operand}%`); break; case "isEmpty": - query.innerJoin("Attendee", "Attendee.id", "Booking.id").where(`Attendee.${key}`, "=", ""); + fullQuery = fullQuery.where(`Attendee.${key}`, "=", ""); break; case "isNotEmpty": - query.innerJoin("Attendee", "Attendee.id", "Booking.id").where(`Attendee.${key}`, "!=", ""); + fullQuery = fullQuery.where(`Attendee.${key}`, "!=", ""); break; default: break; } - return query; + return fullQuery; } function getOrderBy(