@@ -7,6 +7,53 @@ import { MembershipRole } from "@calcom/prisma/enums";
77import { MembershipRepository } from "../repository/membership" ;
88import { TeamRepository } from "../repository/team" ;
99
10+ // Type definition for BookingTimeStatusDenormalized view
11+ export type BookingTimeStatusDenormalized = z . infer < typeof bookingDataSchema > ;
12+
13+ // Helper type for select parameter
14+ export type BookingSelect = {
15+ [ K in keyof BookingTimeStatusDenormalized ] ?: boolean ;
16+ } ;
17+
18+ // Helper type for selected fields
19+ export type SelectedFields < T > = T extends undefined
20+ ? BookingTimeStatusDenormalized
21+ : {
22+ [ K in keyof T as T [ K ] extends true ? K : never ] : K extends keyof BookingTimeStatusDenormalized
23+ ? BookingTimeStatusDenormalized [ K ]
24+ : never ;
25+ } ;
26+
27+ export const bookingDataSchema = z
28+ . object ( {
29+ id : z . number ( ) ,
30+ uid : z . string ( ) ,
31+ eventTypeId : z . number ( ) . nullable ( ) ,
32+ title : z . string ( ) ,
33+ description : z . string ( ) . nullable ( ) ,
34+ startTime : z . date ( ) ,
35+ endTime : z . date ( ) ,
36+ createdAt : z . date ( ) ,
37+ updatedAt : z . date ( ) . nullable ( ) ,
38+ location : z . string ( ) . nullable ( ) ,
39+ paid : z . boolean ( ) ,
40+ status : z . string ( ) , // BookingStatus enum
41+ rescheduled : z . boolean ( ) . nullable ( ) ,
42+ userId : z . number ( ) . nullable ( ) ,
43+ teamId : z . number ( ) . nullable ( ) ,
44+ eventLength : z . number ( ) . nullable ( ) ,
45+ eventParentId : z . number ( ) . nullable ( ) ,
46+ userEmail : z . string ( ) . nullable ( ) ,
47+ userName : z . string ( ) . nullable ( ) ,
48+ userUsername : z . string ( ) . nullable ( ) ,
49+ ratingFeedback : z . string ( ) . nullable ( ) ,
50+ rating : z . number ( ) . nullable ( ) ,
51+ noShowHost : z . boolean ( ) . nullable ( ) ,
52+ isTeamBooking : z . boolean ( ) ,
53+ timeStatus : z . string ( ) . nullable ( ) ,
54+ } )
55+ . strict ( ) ;
56+
1057export const insightsBookingServiceOptionsSchema = z . discriminatedUnion ( "scope" , [
1158 z . object ( {
1259 scope : z . literal ( "user" ) ,
@@ -35,17 +82,28 @@ export type InsightsBookingServicePublicOptions = {
3582
3683export type InsightsBookingServiceOptions = z . infer < typeof insightsBookingServiceOptionsSchema > ;
3784
38- export type InsightsBookingServiceFilterOptions = {
39- eventTypeId ?: number ;
40- memberUserId ?: number ;
41- } ;
85+ export type InsightsBookingServiceFilterOptions = z . infer < typeof insightsBookingServiceFilterOptionsSchema > ;
86+
87+ export const insightsBookingServiceFilterOptionsSchema = z . object ( {
88+ eventTypeId : z . number ( ) . optional ( ) ,
89+ memberUserId : z . number ( ) . optional ( ) ,
90+ dateRange : z
91+ . object ( {
92+ target : z . enum ( [ "createdAt" , "startTime" ] ) ,
93+ startDate : z . string ( ) ,
94+ endDate : z . string ( ) ,
95+ } )
96+ . optional ( ) ,
97+ } ) ;
4298
4399const NOTHING_CONDITION = Prisma . sql `1=0` ;
44100
101+ const bookingDataKeys = new Set ( Object . keys ( bookingDataSchema . shape ) ) ;
102+
45103export class InsightsBookingService {
46104 private prisma : typeof readonlyPrisma ;
47105 private options : InsightsBookingServiceOptions | null ;
48- private filters ? : InsightsBookingServiceFilterOptions ;
106+ private filters : InsightsBookingServiceFilterOptions | null ;
49107 private cachedAuthConditions ?: Prisma . Sql ;
50108 private cachedFilterConditions ?: Prisma . Sql | null ;
51109
@@ -59,26 +117,14 @@ export class InsightsBookingService {
59117 filters ?: InsightsBookingServiceFilterOptions ;
60118 } ) {
61119 this . prisma = prisma ;
62- const validation = insightsBookingServiceOptionsSchema . safeParse ( options ) ;
63- this . options = validation . success ? validation . data : null ;
120+ const optionsValidated = insightsBookingServiceOptionsSchema . safeParse ( options ) ;
121+ this . options = optionsValidated . success ? optionsValidated . data : null ;
64122
65- this . filters = filters ;
123+ const filtersValidated = insightsBookingServiceFilterOptionsSchema . safeParse ( filters ) ;
124+ this . filters = filtersValidated . success ? filtersValidated . data : null ;
66125 }
67126
68- async getBookingsByHourStats ( {
69- startDate,
70- endDate,
71- timeZone,
72- } : {
73- startDate : string ;
74- endDate : string ;
75- timeZone : string ;
76- } ) {
77- // Validate date formats
78- if ( isNaN ( Date . parse ( startDate ) ) || isNaN ( Date . parse ( endDate ) ) ) {
79- throw new Error ( `Invalid date format: ${ startDate } - ${ endDate } ` ) ;
80- }
81-
127+ async getBookingsByHourStats ( { timeZone } : { timeZone : string } ) {
82128 const baseConditions = await this . getBaseConditions ( ) ;
83129
84130 const results = await this . prisma . $queryRaw <
@@ -92,8 +138,6 @@ export class InsightsBookingService {
92138 COUNT(*)::int as "count"
93139 FROM "BookingTimeStatusDenormalized"
94140 WHERE ${ baseConditions }
95- AND "startTime" >= ${ startDate } ::timestamp
96- AND "startTime" <= ${ endDate } ::timestamp
97141 AND "status" = 'accepted'
98142 GROUP BY 1
99143 ORDER BY 1
@@ -109,6 +153,35 @@ export class InsightsBookingService {
109153 } ) ) ;
110154 }
111155
156+ async findAll < TSelect extends BookingSelect | undefined = undefined > ( {
157+ select,
158+ } : {
159+ select ?: TSelect ;
160+ } = { } ) : Promise < Array < SelectedFields < TSelect > > > {
161+ const baseConditions = await this . getBaseConditions ( ) ;
162+
163+ // Build the select clause with validated fields
164+ let selectFields = Prisma . sql `*` ;
165+ if ( select ) {
166+ const keys = Object . keys ( select ) ;
167+ if ( keys . some ( ( key ) => ! bookingDataKeys . has ( key ) ) ) {
168+ throw new Error ( "Invalid select keys provided" ) ;
169+ }
170+
171+ if ( keys . length > 0 ) {
172+ // Use Prisma.sql for each field to ensure proper escaping
173+ const sqlFields = keys . map ( ( field ) => Prisma . sql `"${ Prisma . raw ( field ) } "` ) ;
174+ selectFields = Prisma . join ( sqlFields , ", " ) ;
175+ }
176+ }
177+
178+ return await this . prisma . $queryRaw < Array < SelectedFields < TSelect > > > `
179+ SELECT ${ selectFields }
180+ FROM "BookingTimeStatusDenormalized"
181+ WHERE ${ baseConditions }
182+ ` ;
183+ }
184+
112185 async getBaseConditions ( ) : Promise < Prisma . Sql > {
113186 const authConditions = await this . getAuthorizationConditions ( ) ;
114187 const filterConditions = await this . getFilterConditions ( ) ;
@@ -155,6 +228,23 @@ export class InsightsBookingService {
155228 conditions . push ( Prisma . sql `"userId" = ${ this . filters . memberUserId } ` ) ;
156229 }
157230
231+ // Use dateRange object for date filtering
232+ if ( this . filters . dateRange ) {
233+ const { target, startDate, endDate } = this . filters . dateRange ;
234+ if ( startDate ) {
235+ if ( isNaN ( Date . parse ( startDate ) ) ) {
236+ throw new Error ( `Invalid date format: ${ startDate } ` ) ;
237+ }
238+ conditions . push ( Prisma . sql `"${ Prisma . raw ( target ) } " >= ${ startDate } ::timestamp` ) ;
239+ }
240+ if ( endDate ) {
241+ if ( isNaN ( Date . parse ( endDate ) ) ) {
242+ throw new Error ( `Invalid date format: ${ endDate } ` ) ;
243+ }
244+ conditions . push ( Prisma . sql `"${ Prisma . raw ( target ) } " <= ${ endDate } ::timestamp` ) ;
245+ }
246+ }
247+
158248 if ( conditions . length === 0 ) {
159249 return null ;
160250 }
0 commit comments