Skip to content

Commit f7b201b

Browse files
joeauyeungemrysal
andauthored
fix: Return empty available days if error querying calendar (calcom#22828)
* Return a busy block placeholder if calendar throws an error * Refactor `getCalendarsEvents` to return an object with a success prop * Throw error in `getBusyTimes` if failed to fetch calendar availability * Return empty available days if error getting busy times * yeet. * Type fix * Fix type error in getLuckyUsers * Type fixes * Type fix * Type fix * Fix test * Fix test mocks * Refactor calendars.service to use new calendarBusyTimesQuery --------- Co-authored-by: Alex van Andel <me@alexvanandel.com>
1 parent 82063cc commit f7b201b

8 files changed

Lines changed: 131 additions & 90 deletions

File tree

apps/api/v2/src/ee/calendars/services/calendars.service.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,31 @@ export class CalendarsService {
8383
calendarsToLoad,
8484
userId
8585
);
86-
try {
87-
const calendarBusyTimes = await getBusyCalendarTimes(
88-
this.buildNonDelegationCredentials(credentials),
89-
dateFrom,
90-
dateTo,
91-
composedSelectedCalendars
92-
);
93-
const calendarBusyTimesConverted = calendarBusyTimes.map(
94-
(busyTime: EventBusyDate & { timeZone?: string }) => {
95-
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
96-
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
97-
const busyTimeStartDate = busyTimeStart.toJSDate();
98-
const busyTimeEndDate = busyTimeEnd.toJSDate();
99-
return {
100-
...busyTime,
101-
start: busyTimeStartDate,
102-
end: busyTimeEndDate,
103-
};
104-
}
105-
);
106-
return calendarBusyTimesConverted;
107-
} catch (error) {
86+
const calendarBusyTimesQuery = await getBusyCalendarTimes(
87+
this.buildNonDelegationCredentials(credentials),
88+
dateFrom,
89+
dateTo,
90+
composedSelectedCalendars
91+
);
92+
if (!calendarBusyTimesQuery.success) {
10893
throw new InternalServerErrorException(
10994
"Unable to fetch connected calendars events. Please try again later."
11095
);
11196
}
97+
const calendarBusyTimesConverted = calendarBusyTimesQuery.data.map(
98+
(busyTime: EventBusyDate & { timeZone?: string }) => {
99+
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
100+
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
101+
const busyTimeStartDate = busyTimeStart.toJSDate();
102+
const busyTimeEndDate = busyTimeEnd.toJSDate();
103+
return {
104+
...busyTime,
105+
start: busyTimeStartDate,
106+
end: busyTimeEndDate,
107+
};
108+
}
109+
);
110+
return calendarBusyTimesConverted;
112111
}
113112

114113
async getUniqCalendarCredentials(calendarsToLoad: Calendar[], userId: User["id"]) {

apps/web/test/lib/getSchedule.test.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,12 +1281,15 @@ describe("getSchedule", () => {
12811281
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
12821282
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
12831283

1284-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([
1285-
{
1286-
start: `${plus3DateString}T04:00:00.000Z`,
1287-
end: `${plus3DateString}T05:59:59.000Z`,
1288-
},
1289-
]);
1284+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
1285+
success: true,
1286+
data: [
1287+
{
1288+
start: `${plus3DateString}T04:00:00.000Z`,
1289+
end: `${plus3DateString}T05:59:59.000Z`,
1290+
},
1291+
],
1292+
});
12901293

12911294
const scenarioData = {
12921295
eventTypes: [
@@ -1347,12 +1350,15 @@ describe("getSchedule", () => {
13471350
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
13481351
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
13491352

1350-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([
1351-
{
1352-
start: `${plus3DateString}T04:00:00.000Z`,
1353-
end: `${plus3DateString}T05:59:59.000Z`,
1354-
},
1355-
]);
1353+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
1354+
success: true,
1355+
data: [
1356+
{
1357+
start: `${plus3DateString}T04:00:00.000Z`,
1358+
end: `${plus3DateString}T05:59:59.000Z`,
1359+
},
1360+
],
1361+
});
13561362

13571363
const scenarioData = {
13581364
eventTypes: [
@@ -1421,7 +1427,7 @@ describe("getSchedule", () => {
14211427
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
14221428
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
14231429

1424-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
1430+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
14251431

14261432
const scenarioData = {
14271433
eventTypes: [

packages/lib/CalendarManager.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -245,21 +245,11 @@ export const getBusyCalendarTimes = async (
245245
}
246246

247247
// const months = getMonths(dateFrom, dateTo);
248+
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
249+
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
250+
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
251+
const endDate = dayjs(dateTo).add(14, "hours").format();
248252
try {
249-
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
250-
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
251-
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
252-
const endDate = dayjs(dateTo).add(14, "hours").format();
253-
254-
log.debug(
255-
"getBusyCalendarTimes manipulated dates",
256-
safeStringify({
257-
newStartDate: startDate,
258-
newEndDate: endDate,
259-
oldStartDate: dateFrom,
260-
oldEndDate: dateTo,
261-
})
262-
);
263253
if (includeTimeZone) {
264254
results = await getCalendarsEventsWithTimezones(
265255
deduplicatedCredentials,
@@ -281,8 +271,9 @@ export const getBusyCalendarTimes = async (
281271
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.externalId),
282272
error: safeStringify(e),
283273
});
274+
return { success: false, data: [{ start: startDate, end: endDate, source: "error-placeholder" }] };
284275
}
285-
return results.reduce((acc, availability) => acc.concat(availability), []);
276+
return { success: true, data: results.reduce((acc, availability) => acc.concat(availability), []) };
286277
};
287278

288279
export const createEvent = async (

packages/lib/getBusyTimes.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,24 @@ const _getBusyTimes = async (params: {
179179
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
180180
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
181181
const startConnectedCalendarsGet = performance.now();
182-
const calendarBusyTimes = await getBusyCalendarTimes(
182+
183+
const calendarBusyTimesQuery = await getBusyCalendarTimes(
183184
credentials,
184185
startTime,
185186
endTime,
186187
selectedCalendars,
187188
shouldServeCache
188189
);
190+
191+
if (!calendarBusyTimesQuery.success) {
192+
throw new Error(
193+
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
194+
(calendar) => calendar.id
195+
)}`
196+
);
197+
}
198+
199+
const calendarBusyTimes = calendarBusyTimesQuery.data;
189200
const endConnectedCalendarsGet = performance.now();
190201
logger.debug(
191202
`Connected Calendars get took ${

packages/lib/getUserAvailability.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -411,24 +411,39 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
411411
? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id })
412412
: user.userLevelSelectedCalendars;
413413

414-
const busyTimes = await getBusyTimes({
415-
credentials: user.credentials,
416-
startTime: getBusyTimesStart,
417-
endTime: getBusyTimesEnd,
418-
eventTypeId,
419-
userId: user.id,
420-
userEmail: user.email,
421-
username: `${user.username}`,
422-
beforeEventBuffer,
423-
afterEventBuffer,
424-
selectedCalendars,
425-
seatedEvent: !!eventType?.seatsPerTimeSlot,
426-
rescheduleUid: initialData?.rescheduleUid || null,
427-
duration,
428-
currentBookings: initialData?.currentBookings,
429-
bypassBusyCalendarTimes,
430-
shouldServeCache,
431-
});
414+
let busyTimes = [];
415+
try {
416+
busyTimes = await getBusyTimes({
417+
credentials: user.credentials,
418+
startTime: getBusyTimesStart,
419+
endTime: getBusyTimesEnd,
420+
eventTypeId,
421+
userId: user.id,
422+
userEmail: user.email,
423+
username: `${user.username}`,
424+
beforeEventBuffer,
425+
afterEventBuffer,
426+
selectedCalendars,
427+
seatedEvent: !!eventType?.seatsPerTimeSlot,
428+
rescheduleUid: initialData?.rescheduleUid || null,
429+
duration,
430+
currentBookings: initialData?.currentBookings,
431+
bypassBusyCalendarTimes,
432+
shouldServeCache,
433+
});
434+
} catch (error) {
435+
log.error(`Error fetching busy times for user ${username}:`, error);
436+
return {
437+
busy: [],
438+
timeZone,
439+
dateRanges: [],
440+
oooExcludedDateRanges: [],
441+
workingHours: [],
442+
dateOverrides: [],
443+
currentSeats: [],
444+
datesOutOfOffice: undefined,
445+
};
446+
}
432447

433448
const detailedBusyTimes: EventBusyDetails[] = [
434449
...busyTimes.map((a) => ({

packages/lib/server/getLuckyUser.test.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ it("can find lucky user with maximize availability", async () => {
5555
}),
5656
];
5757

58-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
58+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
5959
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
6060

6161
// TODO: we may be able to use native prisma generics somehow?
@@ -107,7 +107,7 @@ it("can find lucky user with maximize availability and priority ranking", async
107107
}),
108108
];
109109

110-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
110+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
111111
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
112112

113113
// TODO: we may be able to use native prisma generics somehow?
@@ -289,7 +289,7 @@ describe("maximize availability and weights", () => {
289289
}),
290290
];
291291

292-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
292+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
293293
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
294294
prismaMock.user.findMany.mockResolvedValue(users);
295295
prismaMock.host.findMany.mockResolvedValue([]);
@@ -392,7 +392,7 @@ describe("maximize availability and weights", () => {
392392
}),
393393
];
394394

395-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
395+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
396396
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
397397
prismaMock.user.findMany.mockResolvedValue(users);
398398
prismaMock.host.findMany.mockResolvedValue([]);
@@ -500,7 +500,7 @@ describe("maximize availability and weights", () => {
500500
}),
501501
];
502502

503-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
503+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
504504
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
505505
prismaMock.user.findMany.mockResolvedValue(users);
506506
prismaMock.host.findMany.mockResolvedValue([]);
@@ -600,7 +600,7 @@ describe("maximize availability and weights", () => {
600600
},
601601
];
602602

603-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
603+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
604604

605605
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([
606606
{
@@ -707,13 +707,16 @@ describe("maximize availability and weights", () => {
707707
];
708708

709709
CalendarManagerMock.getBusyCalendarTimes
710-
.mockResolvedValueOnce([
711-
{
712-
start: dayjs().utc().startOf("month").toDate(),
713-
end: dayjs().utc().startOf("month").add(3, "day").toDate(),
714-
timeZone: "UTC",
715-
},
716-
])
710+
.mockResolvedValueOnce({
711+
success: true,
712+
data: [
713+
{
714+
start: dayjs().utc().startOf("month").toDate(),
715+
end: dayjs().utc().startOf("month").add(3, "day").toDate(),
716+
timeZone: "UTC",
717+
},
718+
],
719+
})
717720
.mockResolvedValue([]);
718721

719722
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
@@ -817,7 +820,7 @@ describe("maximize availability and weights", () => {
817820
},
818821
];
819822

820-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
823+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
821824
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
822825

823826
// TODO: we may be able to use native prisma generics somehow?
@@ -1279,7 +1282,7 @@ describe("attribute weights and virtual queues", () => {
12791282
chosenRouteId: routeId,
12801283
};
12811284

1282-
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
1285+
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
12831286
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
12841287

12851288
prismaMock.user.findMany.mockResolvedValue(users);

packages/lib/server/getLuckyUser.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ async function getCalendarBusyTimesOfInterval(
456456
rrTimestampBasis: RRTimestampBasis,
457457
meetingStartTime?: Date
458458
): Promise<{ userId: number; busyTimes: (EventBusyDate & { timeZone?: string })[] }[]> {
459-
return Promise.all(
459+
const usersBusyTimesQuery = await Promise.all(
460460
usersWithCredentials.map((user) =>
461461
getBusyCalendarTimes(
462462
user.credentials,
@@ -465,12 +465,19 @@ async function getCalendarBusyTimesOfInterval(
465465
user.userLevelSelectedCalendars,
466466
true,
467467
true
468-
).then((busyTimes) => ({
469-
userId: user.id,
470-
busyTimes,
471-
}))
468+
)
472469
)
473470
);
471+
472+
return usersBusyTimesQuery.reduce((usersBusyTime, userBusyTimeQuery, index) => {
473+
if (userBusyTimeQuery.success) {
474+
usersBusyTime.push({
475+
userId: usersWithCredentials[index].id,
476+
busyTimes: userBusyTimeQuery.data,
477+
});
478+
}
479+
return usersBusyTime;
480+
}, [] as { userId: number; busyTimes: Awaited<ReturnType<typeof getBusyCalendarTimes>>["data"] }[]);
474481
}
475482

476483
async function getBookingsOfInterval({

packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,22 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => {
8484
});
8585

8686
// get all clanedar services
87-
const calendarBusyTimes = await getBusyCalendarTimes(
87+
const calendarBusyTimesQuery = await getBusyCalendarTimes(
8888
credentials,
8989
dateFrom,
9090
dateTo,
9191
composedSelectedCalendars
9292
);
9393

94+
if (!calendarBusyTimesQuery.success) {
95+
throw new TRPCError({
96+
code: "INTERNAL_SERVER_ERROR",
97+
message: "Failed to fetch busy calendar times",
98+
});
99+
}
100+
101+
const calendarBusyTimes = calendarBusyTimesQuery.data;
102+
94103
// Convert to users timezone
95104

96105
const userTimeZone = input.loggedInUsersTz;

0 commit comments

Comments
 (0)