From c3985e3efb4b331b834f8b6f737866980e8a4c87 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Fri, 12 Sep 2025 00:16:12 +0200 Subject: [PATCH] fix: including Prisma.sql for Prisma v6 upgrade (#23775) --- .../service/InsightsBookingBaseService.ts | 190 ++++++++++-------- .../service/InsightsRoutingBaseService.ts | 109 ++++++---- 2 files changed, 172 insertions(+), 127 deletions(-) diff --git a/packages/lib/server/service/InsightsBookingBaseService.ts b/packages/lib/server/service/InsightsBookingBaseService.ts index 942a0d18b2..6a8f9ea045 100644 --- a/packages/lib/server/service/InsightsBookingBaseService.ts +++ b/packages/lib/server/service/InsightsBookingBaseService.ts @@ -12,8 +12,8 @@ import { isNumberFilterValue, } from "@calcom/features/data-table/lib/utils"; import type { DateRange } from "@calcom/features/insights/server/insightsDateUtils"; -import { Prisma } from "@calcom/prisma/client"; import type { PrismaClient } from "@calcom/prisma"; +import { Prisma } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRepository } from "../repository/membership"; @@ -168,12 +168,7 @@ export class InsightsBookingBaseService { async getBookingsByHourStats({ timeZone }: { timeZone: string }) { const baseConditions = await this.getBaseConditions(); - const results = await this.prisma.$queryRaw< - Array<{ - hour: string; - count: number; - }> - >` + const query = Prisma.sql` SELECT EXTRACT(HOUR FROM ("startTime" AT TIME ZONE 'UTC' AT TIME ZONE ${timeZone}))::int as "hour", COUNT(*)::int as "count" @@ -184,6 +179,13 @@ export class InsightsBookingBaseService { ORDER BY 1 `; + const results = await this.prisma.$queryRaw< + Array<{ + hour: string; + count: number; + }> + >(query); + // Create a map of results by hour for easy lookup const resultsMap = new Map(results.map((row) => [Number(row.hour), row.count])); @@ -216,11 +218,13 @@ export class InsightsBookingBaseService { } } - return await this.prisma.$queryRaw>>` + const query = Prisma.sql` SELECT ${selectFields} FROM "BookingTimeStatusDenormalized" WHERE ${baseConditions} `; + + return await this.prisma.$queryRaw>>(query); } async getBaseConditions(): Promise { @@ -449,33 +453,17 @@ export class InsightsBookingBaseService { const baseConditions = await this.getBaseConditions(); // Get total count first - const totalCountResult = await this.prisma.$queryRaw<[{ count: number }]>` + const totalCountQuery = Prisma.sql` SELECT COUNT(*)::int as count FROM "BookingTimeStatusDenormalized" WHERE ${baseConditions} `; + + const totalCountResult = await this.prisma.$queryRaw<[{ count: number }]>(totalCountQuery); const totalCount = totalCountResult[0]?.count || 0; // 1. Get booking data from BookingTimeStatusDenormalized - const csvData = await this.prisma.$queryRaw< - Array<{ - id: number; - uid: string | null; - title: string; - createdAt: Date; - timeStatus: string; - eventTypeId: number | null; - eventLength: number; - startTime: Date; - endTime: Date; - paid: boolean; - userEmail: string; - userUsername: string; - rating: number | null; - ratingFeedback: string | null; - noShowHost: boolean; - }> - >` + const csvDataQuery = Prisma.sql` SELECT "id", "uid", @@ -499,6 +487,26 @@ export class InsightsBookingBaseService { OFFSET ${offset} `; + const csvData = await this.prisma.$queryRaw< + Array<{ + id: number; + uid: string | null; + title: string; + createdAt: Date; + timeStatus: string; + eventTypeId: number | null; + eventLength: number; + startTime: Date; + endTime: Date; + paid: boolean; + userEmail: string; + userUsername: string; + rating: number | null; + ratingFeedback: string | null; + noShowHost: boolean; + }> + >(csvDataQuery); + if (csvData.length === 0) { return { data: csvData, total: totalCount }; } @@ -641,15 +649,7 @@ export class InsightsBookingBaseService { const baseConditions = await this.getBaseConditions(); - const data = await this.prisma.$queryRaw< - { - date: Date; - bookingsCount: number; - timeStatus: string; - noShowHost: boolean; - noShowGuests: number; - }[] - >` + const query = Prisma.sql` WITH booking_stats AS ( SELECT DATE("createdAt" AT TIME ZONE ${timeZone}) as "date", @@ -688,6 +688,16 @@ export class InsightsBookingBaseService { ORDER BY bs."date" `; + const data = await this.prisma.$queryRaw< + { + date: Date; + bookingsCount: number; + timeStatus: string; + noShowHost: boolean; + noShowGuests: number; + }[] + >(query); + // Initialize aggregate object with zero counts for all date ranges const aggregate: { [date: string]: { @@ -777,12 +787,7 @@ export class InsightsBookingBaseService { async getPopularEventsStats() { const baseConditions = await this.getBaseConditions(); - const bookingsFromSelected = await this.prisma.$queryRaw< - Array<{ - eventTypeId: number; - count: number; - }> - >` + const query = Prisma.sql` SELECT "eventTypeId", COUNT(id)::int as count @@ -793,6 +798,13 @@ export class InsightsBookingBaseService { LIMIT 10 `; + const bookingsFromSelected = await this.prisma.$queryRaw< + Array<{ + eventTypeId: number; + count: number; + }> + >(query); + const eventTypeIds = bookingsFromSelected.map((booking) => booking.eventTypeId); if (eventTypeIds.length === 0) { @@ -866,12 +878,7 @@ export class InsightsBookingBaseService { additionalCondition = Prisma.sql`AND status = 'accepted'`; } - const bookingsFromTeam = await this.prisma.$queryRaw< - Array<{ - userId: number; - count: number; - }> - >` + const query = Prisma.sql` SELECT "userId", COUNT(id)::int as count @@ -882,6 +889,13 @@ export class InsightsBookingBaseService { LIMIT 10 `; + const bookingsFromTeam = await this.prisma.$queryRaw< + Array<{ + userId: number; + count: number; + }> + >(query); + if (bookingsFromTeam.length === 0) { return []; } @@ -927,12 +941,7 @@ export class InsightsBookingBaseService { async getMembersRatingStats(sortOrder: "ASC" | "DESC" = "DESC"): Promise { const baseConditions = await this.getBaseConditions(); - const bookingsFromTeam = await this.prisma.$queryRaw< - Array<{ - userId: number; - count: number; - }> - >` + const query = Prisma.sql` SELECT "userId", AVG("rating")::float as "count" @@ -943,6 +952,13 @@ export class InsightsBookingBaseService { LIMIT 10 `; + const bookingsFromTeam = await this.prisma.$queryRaw< + Array<{ + userId: number; + count: number; + }> + >(query); + if (bookingsFromTeam.length === 0) { return []; } @@ -988,13 +1004,7 @@ export class InsightsBookingBaseService { async getRecentRatingsStats() { const baseConditions = await this.getBaseConditions(); - const bookingsFromTeam = await this.prisma.$queryRaw< - Array<{ - userId: number | null; - rating: number | null; - ratingFeedback: string | null; - }> - >` + const query = Prisma.sql` SELECT "userId", "rating", @@ -1005,6 +1015,14 @@ export class InsightsBookingBaseService { LIMIT 10 `; + const bookingsFromTeam = await this.prisma.$queryRaw< + Array<{ + userId: number | null; + rating: number | null; + ratingFeedback: string | null; + }> + >(query); + if (bookingsFromTeam.length === 0) { return []; } @@ -1062,19 +1080,7 @@ export class InsightsBookingBaseService { async getBookingStats() { const baseConditions = await this.getBaseConditions(); - const stats = await this.prisma.$queryRaw< - Array<{ - total_bookings: bigint; - completed_bookings: bigint; - rescheduled_bookings: bigint; - cancelled_bookings: bigint; - no_show_host_bookings: bigint; - avg_rating: number | null; - total_ratings: bigint; - ratings_above_3: bigint; - no_show_guests: bigint; - }> - >` + const query = Prisma.sql` WITH booking_stats AS ( SELECT COUNT(*) as total_bookings, @@ -1107,6 +1113,20 @@ export class InsightsBookingBaseService { FROM booking_stats bs, guest_stats gs `; + const stats = await this.prisma.$queryRaw< + Array<{ + total_bookings: bigint; + completed_bookings: bigint; + rescheduled_bookings: bigint; + cancelled_bookings: bigint; + no_show_host_bookings: bigint; + avg_rating: number | null; + total_ratings: bigint; + ratings_above_3: bigint; + no_show_guests: bigint; + }> + >(query); + const rawStats = stats[0]; return rawStats ? { @@ -1136,15 +1156,7 @@ export class InsightsBookingBaseService { async getRecentNoShowGuests() { const baseConditions = await this.getBaseConditions(); - const recentNoShowBookings = await this.prisma.$queryRaw< - Array<{ - bookingId: number; - startTime: Date; - eventTypeName: string; - guestName: string; - guestEmail: string; - }> - >` + const query = Prisma.sql` WITH booking_attendee_stats AS ( SELECT b.id as booking_id, @@ -1182,6 +1194,16 @@ export class InsightsBookingBaseService { LIMIT 10 `; + const recentNoShowBookings = await this.prisma.$queryRaw< + Array<{ + bookingId: number; + startTime: Date; + eventTypeName: string; + guestName: string; + guestEmail: string; + }> + >(query); + return recentNoShowBookings; } diff --git a/packages/lib/server/service/InsightsRoutingBaseService.ts b/packages/lib/server/service/InsightsRoutingBaseService.ts index 7c07c4c50b..d410a1c2fe 100644 --- a/packages/lib/server/service/InsightsRoutingBaseService.ts +++ b/packages/lib/server/service/InsightsRoutingBaseService.ts @@ -183,14 +183,7 @@ export class InsightsRoutingBaseService { .reduce((acc, curr) => Prisma.sql`${acc} ${curr}`); // Single query to get all data grouped by date ranges using CTE - const results = await this.prisma.$queryRaw< - Array<{ - dateRange: string | null; - totalSubmissions: bigint; - successfulRoutings: bigint; - acceptedBookings: bigint; - }> - >` + const query = Prisma.sql` WITH date_ranged_data AS ( SELECT CASE ${caseStatements} @@ -212,6 +205,15 @@ export class InsightsRoutingBaseService { ORDER BY "dateRange" `; + const results = await this.prisma.$queryRaw< + Array<{ + dateRange: string | null; + totalSubmissions: bigint; + successfulRoutings: bigint; + acceptedBookings: bigint; + }> + >(query); + // Create a map of results by dateRange for easy lookup const resultsMap = new Map( results @@ -265,16 +267,18 @@ export class InsightsRoutingBaseService { const orderByClause = this.buildOrderByClause(sorting); // Get total count - const totalCountResult = await this.prisma.$queryRaw>` + const totalCountQuery = Prisma.sql` SELECT COUNT(*) as count FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions} `; + const totalCountResult = await this.prisma.$queryRaw>(totalCountQuery); + const totalCount = Number(totalCountResult[0]?.count || 0); // Get paginated data with JSON aggregation for attendees and fields - const data = await this.prisma.$queryRaw>` + const dataQuery = Prisma.sql` SELECT rfrd."id", rfrd."uuid", @@ -341,6 +345,8 @@ export class InsightsRoutingBaseService { OFFSET ${offset} `; + const data = await this.prisma.$queryRaw>(dataQuery); + return { total: totalCount, data, @@ -359,19 +365,25 @@ export class InsightsRoutingBaseService { } // Get total count - const totalResult = await this.prisma.$queryRaw>` + const totalQuery = Prisma.sql` SELECT COUNT(*) as count FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions} `; + const totalResult = await this.prisma.$queryRaw>(totalQuery); + // Get total without booking count - const totalWithoutBookingResult = await this.prisma.$queryRaw>` + const totalWithoutBookingQuery = Prisma.sql` SELECT COUNT(*) as count FROM "RoutingFormResponseDenormalized" rfrd WHERE (${baseConditions}) AND ("bookingUid" IS NULL) `; + const totalWithoutBookingResult = await this.prisma.$queryRaw>( + totalWithoutBookingQuery + ); + const total = Number(totalResult[0]?.count || 0); const totalWithoutBooking = Number(totalWithoutBookingResult[0]?.count || 0); @@ -437,14 +449,7 @@ export class InsightsRoutingBaseService { : Prisma.sql`1 = 1`; // Get users who have been routed to during the period - const usersQuery = await this.prisma.$queryRaw< - Array<{ - id: number; - name: string | null; - email: string | null; - avatarUrl: string | null; - }> - >` + const usersQuerySql = Prisma.sql` SELECT DISTINCT ON ("bookingUserId") "bookingUserId" as id, "bookingUserName" as name, @@ -461,6 +466,15 @@ export class InsightsRoutingBaseService { ${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty} `; + const usersQuery = await this.prisma.$queryRaw< + Array<{ + id: number; + name: string | null; + email: string | null; + avatarUrl: string | null; + }> + >(usersQuerySql); + const users = usersQuery; // Return early if no users found @@ -478,13 +492,7 @@ export class InsightsRoutingBaseService { } // Get periods with pagination - const periodStats = await this.prisma.$queryRaw< - Array<{ - userId: number; - period_start: Date; - total: number; - }> - >` + const periodStatsQuery = Prisma.sql` WITH RECURSIVE date_range AS ( SELECT date_trunc(${dayjsPeriod}, ${startDate}::timestamp) as date UNION ALL @@ -545,13 +553,16 @@ export class InsightsRoutingBaseService { ORDER BY c.period_start ASC, c."userId" ASC `; - // Get statistics for the entire period for comparison - const statsQuery = await this.prisma.$queryRaw< + const periodStats = await this.prisma.$queryRaw< Array<{ userId: number; - total_bookings: number; + period_start: Date; + total: number; }> - >` + >(periodStatsQuery); + + // Get statistics for the entire period for comparison + const statsQuerySql = Prisma.sql` SELECT "bookingUserId" as "userId", COUNT(DISTINCT "bookingId")::integer as total_bookings @@ -564,6 +575,13 @@ export class InsightsRoutingBaseService { ORDER BY total_bookings ASC `; + const statsQuery = await this.prisma.$queryRaw< + Array<{ + userId: number; + total_bookings: number; + }> + >(statsQuerySql); + // Calculate average and median const average = statsQuery.reduce((sum, stat) => sum + Number(stat.total_bookings), 0) / statsQuery.length || 0; @@ -625,7 +643,10 @@ export class InsightsRoutingBaseService { if (!userStatsMap.has(userId)) { userStatsMap.set(userId, {}); } - userStatsMap.get(userId)![stat.period_start.toISOString()] = stat.total; + const userStat = userStatsMap.get(userId); + if (userStat) { + userStat[stat.period_start.toISOString()] = stat.total; + } }); return data.users.data.map((user) => { @@ -940,17 +961,7 @@ export class InsightsRoutingBaseService { const baseConditions = await this.getBaseConditions(); // Get failed bookings (responses without a successful booking) grouped by form, field, and option - const result = await this.prisma.$queryRaw< - { - formId: string; - formName: string; - fieldId: string; - fieldLabel: string; - optionId: string; - optionLabel: string; - count: number; - }[] - >` + const query = Prisma.sql` WITH form_fields AS ( SELECT DISTINCT rfrd."formId" as form_id, @@ -997,6 +1008,18 @@ export class InsightsRoutingBaseService { ORDER BY count DESC `; + const result = await this.prisma.$queryRaw< + { + formId: string; + formName: string; + fieldId: string; + fieldLabel: string; + optionId: string; + optionLabel: string; + count: number; + }[] + >(query); + // First group by form and field const groupedByFormAndField = result.reduce((acc, curr) => { const formKey = curr.formName;