Skip to content

Commit cabd459

Browse files
authored
chore: slots time range validation to zod (#22942)
* chore: Move date range validation to zod, removing TRPCError * This is not a BAD_REQUEST but an internal error, should not happen * Add a check to ensure the given start time is before end time
1 parent 33aedba commit cabd459

File tree

2 files changed

+49
-61
lines changed

2 files changed

+49
-61
lines changed

packages/trpc/server/routers/viewer/slots/types.ts

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,45 @@ import { z } from "zod";
33

44
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
55

6-
export const getScheduleSchema = z
7-
.object({
8-
// startTime ISOString
9-
startTime: z.string(),
10-
// endTime ISOString
11-
endTime: z.string(),
12-
// Event type ID
13-
eventTypeId: z.coerce.number().int().optional(),
14-
// Event type slug
15-
eventTypeSlug: z.string().optional(),
16-
// invitee timezone
17-
timeZone: timeZoneSchema.optional(),
18-
// or list of users (for dynamic events)
19-
usernameList: z.array(z.string()).min(1).optional(),
20-
debug: z.boolean().optional(),
21-
// to handle event types with multiple duration options
22-
duration: z
23-
.string()
24-
.optional()
25-
.transform((val) => val && parseInt(val)),
26-
rescheduleUid: z.string().nullish(),
27-
// whether to do team event or user event
28-
isTeamEvent: z.boolean().optional().default(false),
29-
orgSlug: z.string().nullish(),
30-
teamMemberEmail: z.string().nullish(),
31-
routedTeamMemberIds: z.array(z.number()).nullish(),
32-
skipContactOwner: z.boolean().nullish(),
33-
_enableTroubleshooter: z.boolean().optional(),
34-
_bypassCalendarBusyTimes: z.boolean().optional(),
35-
_shouldServeCache: z.boolean().optional(),
36-
routingFormResponseId: z.number().optional(),
37-
queuedFormResponseId: z.string().nullish(),
38-
email: z.string().nullish(),
39-
})
6+
const isValidDateString = (val: string) => !isNaN(Date.parse(val));
7+
8+
export const getScheduleSchemaObject = z.object({
9+
startTime: z.string().refine(isValidDateString, {
10+
message: "startTime must be a valid date string",
11+
}),
12+
endTime: z.string().refine(isValidDateString, {
13+
message: "endTime must be a valid date string",
14+
}),
15+
// Event type ID
16+
eventTypeId: z.coerce.number().int().optional(),
17+
// Event type slug
18+
eventTypeSlug: z.string().optional(),
19+
// invitee timezone
20+
timeZone: timeZoneSchema.optional(),
21+
// or list of users (for dynamic events)
22+
usernameList: z.array(z.string()).min(1).optional(),
23+
debug: z.boolean().optional(),
24+
// to handle event types with multiple duration options
25+
duration: z
26+
.string()
27+
.optional()
28+
.transform((val) => val && parseInt(val)),
29+
rescheduleUid: z.string().nullish(),
30+
// whether to do team event or user event
31+
isTeamEvent: z.boolean().optional().default(false),
32+
orgSlug: z.string().nullish(),
33+
teamMemberEmail: z.string().nullish(),
34+
routedTeamMemberIds: z.array(z.number()).nullish(),
35+
skipContactOwner: z.boolean().nullish(),
36+
_enableTroubleshooter: z.boolean().optional(),
37+
_bypassCalendarBusyTimes: z.boolean().optional(),
38+
_shouldServeCache: z.boolean().optional(),
39+
routingFormResponseId: z.number().optional(),
40+
queuedFormResponseId: z.string().nullish(),
41+
email: z.string().nullish(),
42+
});
43+
44+
export const getScheduleSchema = getScheduleSchemaObject
4045
.transform((val) => {
4146
// Need this so we can pass a single username in the query string form public API
4247
if (val.usernameList) {
@@ -50,7 +55,11 @@ export const getScheduleSchema = z
5055
.refine(
5156
(data) => !!data.eventTypeId || (!!data.usernameList && !!data.eventTypeSlug),
5257
"You need to either pass an eventTypeId OR an usernameList/eventTypeSlug combination"
53-
);
58+
)
59+
.refine(({ startTime, endTime }) => new Date(endTime).getTime() > new Date(startTime).getTime(), {
60+
message: "endTime must be after startTime",
61+
path: ["endTime"],
62+
});
5463

5564
export const reserveSlotSchema = z
5665
.object({
@@ -82,7 +91,7 @@ export interface ContextForGetSchedule extends Record<string, unknown> {
8291
req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined;
8392
}
8493

85-
export type TGetScheduleInputSchema = z.infer<typeof getScheduleSchema>;
94+
export type TGetScheduleInputSchema = z.infer<typeof getScheduleSchemaObject>;
8695
export const ZGetScheduleInputSchema = getScheduleSchema;
8796

8897
export type GetScheduleOptions = {

packages/trpc/server/routers/viewer/slots/util.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,8 @@ export class AvailableSlotsService {
186186
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
187187
// For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic
188188
if (!input.eventTypeSlug) {
189-
throw new TRPCError({
190-
message: "eventTypeSlug is required for dynamic booking",
191-
code: "BAD_REQUEST",
192-
});
189+
// never happens as it's guarded by our Zod Schema refine, but for clear type safety we throw an Error if the eventTypeSlug isn't given.
190+
throw new Error("Event type slug is required in dynamic booking.");
193191
}
194192
const dynamicEventType = getDefaultEvent(input.eventTypeSlug);
195193

@@ -634,19 +632,6 @@ export class AvailableSlotsService {
634632

635633
limitManager.mergeBusyTimes(globalLimitManager);
636634

637-
const bookingLimitsParams = {
638-
bookings: userBusyTimes,
639-
bookingLimits,
640-
dateFrom,
641-
dateTo,
642-
limitManager,
643-
rescheduleUid,
644-
teamId,
645-
user,
646-
includeManagedEvents,
647-
timeZone,
648-
};
649-
650635
for (const key of descendingLimitKeys) {
651636
const limit = bookingLimits?.[key];
652637
if (!limit) continue;
@@ -1011,10 +996,6 @@ export class AvailableSlotsService {
1011996
);
1012997
const endTime =
1013998
input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone);
1014-
1015-
if (!startTime.isValid() || !endTime.isValid()) {
1016-
throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" });
1017-
}
1018999
// when an empty array is given we should prefer to have it handled as if this wasn't given at all
10191000
// we don't want to return no availability in this case.
10201001
const routedTeamMemberIds = input.routedTeamMemberIds ?? [];
@@ -1164,11 +1145,9 @@ export class AvailableSlotsService {
11641145
scheduleId: eventType.restrictionScheduleId,
11651146
});
11661147
if (restrictionSchedule) {
1148+
// runtime error preventing misconfiguration when restrictionSchedule timeZone must be used.
11671149
if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) {
1168-
throw new TRPCError({
1169-
message: "No timezone is set for the restricted schedule",
1170-
code: "BAD_REQUEST",
1171-
});
1150+
throw new Error("No timezone is set for the restricted schedule");
11721151
}
11731152

11741153
const restrictionTimezone = eventType.useBookerTimezone

0 commit comments

Comments
 (0)