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
This commit is contained in:
1 parent
33aedbae20
commit
cabd459682
2 files changed
+49
-61
No files matched your search
@@ -3,40 +3,45 @@ import { z } from "zod";
|
||||
|
||||
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
|
||||
|
||||
export const getScheduleSchema = z
|
||||
.object({
|
||||
// startTime ISOString
|
||||
startTime: z.string(),
|
||||
// endTime ISOString
|
||||
endTime: z.string(),
|
||||
// Event type ID
|
||||
eventTypeId: z.coerce.number().int().optional(),
|
||||
// Event type slug
|
||||
eventTypeSlug: z.string().optional(),
|
||||
// invitee timezone
|
||||
timeZone: timeZoneSchema.optional(),
|
||||
// or list of users (for dynamic events)
|
||||
usernameList: z.array(z.string()).min(1).optional(),
|
||||
debug: z.boolean().optional(),
|
||||
// to handle event types with multiple duration options
|
||||
duration: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val && parseInt(val)),
|
||||
rescheduleUid: z.string().nullish(),
|
||||
// whether to do team event or user event
|
||||
isTeamEvent: z.boolean().optional().default(false),
|
||||
orgSlug: z.string().nullish(),
|
||||
teamMemberEmail: z.string().nullish(),
|
||||
routedTeamMemberIds: z.array(z.number()).nullish(),
|
||||
skipContactOwner: z.boolean().nullish(),
|
||||
_enableTroubleshooter: z.boolean().optional(),
|
||||
_bypassCalendarBusyTimes: z.boolean().optional(),
|
||||
_shouldServeCache: z.boolean().optional(),
|
||||
routingFormResponseId: z.number().optional(),
|
||||
queuedFormResponseId: z.string().nullish(),
|
||||
email: z.string().nullish(),
|
||||
})
|
||||
const isValidDateString = (val: string) => !isNaN(Date.parse(val));
|
||||
|
||||
export const getScheduleSchemaObject = z.object({
|
||||
startTime: z.string().refine(isValidDateString, {
|
||||
message: "startTime must be a valid date string",
|
||||
}),
|
||||
endTime: z.string().refine(isValidDateString, {
|
||||
message: "endTime must be a valid date string",
|
||||
}),
|
||||
// Event type ID
|
||||
eventTypeId: z.coerce.number().int().optional(),
|
||||
// Event type slug
|
||||
eventTypeSlug: z.string().optional(),
|
||||
// invitee timezone
|
||||
timeZone: timeZoneSchema.optional(),
|
||||
// or list of users (for dynamic events)
|
||||
usernameList: z.array(z.string()).min(1).optional(),
|
||||
debug: z.boolean().optional(),
|
||||
// to handle event types with multiple duration options
|
||||
duration: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val && parseInt(val)),
|
||||
rescheduleUid: z.string().nullish(),
|
||||
// whether to do team event or user event
|
||||
isTeamEvent: z.boolean().optional().default(false),
|
||||
orgSlug: z.string().nullish(),
|
||||
teamMemberEmail: z.string().nullish(),
|
||||
routedTeamMemberIds: z.array(z.number()).nullish(),
|
||||
skipContactOwner: z.boolean().nullish(),
|
||||
_enableTroubleshooter: z.boolean().optional(),
|
||||
_bypassCalendarBusyTimes: z.boolean().optional(),
|
||||
_shouldServeCache: z.boolean().optional(),
|
||||
routingFormResponseId: z.number().optional(),
|
||||
queuedFormResponseId: z.string().nullish(),
|
||||
email: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const getScheduleSchema = getScheduleSchemaObject
|
||||
.transform((val) => {
|
||||
// Need this so we can pass a single username in the query string form public API
|
||||
if (val.usernameList) {
|
||||
@@ -50,7 +55,11 @@ export const getScheduleSchema = z
|
||||
.refine(
|
||||
(data) => !!data.eventTypeId || (!!data.usernameList && !!data.eventTypeSlug),
|
||||
"You need to either pass an eventTypeId OR an usernameList/eventTypeSlug combination"
|
||||
);
|
||||
)
|
||||
.refine(({ startTime, endTime }) => new Date(endTime).getTime() > new Date(startTime).getTime(), {
|
||||
message: "endTime must be after startTime",
|
||||
path: ["endTime"],
|
||||
});
|
||||
|
||||
export const reserveSlotSchema = z
|
||||
.object({
|
||||
@@ -82,7 +91,7 @@ export interface ContextForGetSchedule extends Record<string, unknown> {
|
||||
req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined;
|
||||
}
|
||||
|
||||
export type TGetScheduleInputSchema = z.infer<typeof getScheduleSchema>;
|
||||
export type TGetScheduleInputSchema = z.infer<typeof getScheduleSchemaObject>;
|
||||
export const ZGetScheduleInputSchema = getScheduleSchema;
|
||||
|
||||
export type GetScheduleOptions = {
|
||||
|
||||
@@ -186,10 +186,8 @@ export class AvailableSlotsService {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
// 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
|
||||
if (!input.eventTypeSlug) {
|
||||
throw new TRPCError({
|
||||
message: "eventTypeSlug is required for dynamic booking",
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
// 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.
|
||||
throw new Error("Event type slug is required in dynamic booking.");
|
||||
}
|
||||
const dynamicEventType = getDefaultEvent(input.eventTypeSlug);
|
||||
|
||||
@@ -634,19 +632,6 @@ export class AvailableSlotsService {
|
||||
|
||||
limitManager.mergeBusyTimes(globalLimitManager);
|
||||
|
||||
const bookingLimitsParams = {
|
||||
bookings: userBusyTimes,
|
||||
bookingLimits,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
limitManager,
|
||||
rescheduleUid,
|
||||
teamId,
|
||||
user,
|
||||
includeManagedEvents,
|
||||
timeZone,
|
||||
};
|
||||
|
||||
for (const key of descendingLimitKeys) {
|
||||
const limit = bookingLimits?.[key];
|
||||
if (!limit) continue;
|
||||
@@ -1011,10 +996,6 @@ export class AvailableSlotsService {
|
||||
);
|
||||
const endTime =
|
||||
input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone);
|
||||
|
||||
if (!startTime.isValid() || !endTime.isValid()) {
|
||||
throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" });
|
||||
}
|
||||
// when an empty array is given we should prefer to have it handled as if this wasn't given at all
|
||||
// we don't want to return no availability in this case.
|
||||
const routedTeamMemberIds = input.routedTeamMemberIds ?? [];
|
||||
@@ -1164,11 +1145,9 @@ export class AvailableSlotsService {
|
||||
scheduleId: eventType.restrictionScheduleId,
|
||||
});
|
||||
if (restrictionSchedule) {
|
||||
// runtime error preventing misconfiguration when restrictionSchedule timeZone must be used.
|
||||
if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) {
|
||||
throw new TRPCError({
|
||||
message: "No timezone is set for the restricted schedule",
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
throw new Error("No timezone is set for the restricted schedule");
|
||||
}
|
||||
|
||||
const restrictionTimezone = eventType.useBookerTimezone
|
||||
|
||||
Reference in new issue
Block a user