fix: Return empty available days if error querying calendar (#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>
This commit is contained in:
co-authored by
Alex van Andel
parent
82063cc9a1
commit
f7b201b0b5
@@ -83,32 +83,31 @@ export class CalendarsService {
|
||||
calendarsToLoad,
|
||||
userId
|
||||
);
|
||||
try {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
this.buildNonDelegationCredentials(credentials),
|
||||
dateFrom,
|
||||
dateTo,
|
||||
composedSelectedCalendars
|
||||
);
|
||||
const calendarBusyTimesConverted = calendarBusyTimes.map(
|
||||
(busyTime: EventBusyDate & { timeZone?: string }) => {
|
||||
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
||||
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
||||
const busyTimeStartDate = busyTimeStart.toJSDate();
|
||||
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
||||
return {
|
||||
...busyTime,
|
||||
start: busyTimeStartDate,
|
||||
end: busyTimeEndDate,
|
||||
};
|
||||
}
|
||||
);
|
||||
return calendarBusyTimesConverted;
|
||||
} catch (error) {
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
this.buildNonDelegationCredentials(credentials),
|
||||
dateFrom,
|
||||
dateTo,
|
||||
composedSelectedCalendars
|
||||
);
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new InternalServerErrorException(
|
||||
"Unable to fetch connected calendars events. Please try again later."
|
||||
);
|
||||
}
|
||||
const calendarBusyTimesConverted = calendarBusyTimesQuery.data.map(
|
||||
(busyTime: EventBusyDate & { timeZone?: string }) => {
|
||||
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
||||
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
||||
const busyTimeStartDate = busyTimeStart.toJSDate();
|
||||
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
||||
return {
|
||||
...busyTime,
|
||||
start: busyTimeStartDate,
|
||||
end: busyTimeEndDate,
|
||||
};
|
||||
}
|
||||
);
|
||||
return calendarBusyTimesConverted;
|
||||
}
|
||||
|
||||
async getUniqCalendarCredentials(calendarsToLoad: Calendar[], userId: User["id"]) {
|
||||
|
||||
@@ -1281,12 +1281,15 @@ describe("getSchedule", () => {
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([
|
||||
{
|
||||
start: `${plus3DateString}T04:00:00.000Z`,
|
||||
end: `${plus3DateString}T05:59:59.000Z`,
|
||||
},
|
||||
]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
start: `${plus3DateString}T04:00:00.000Z`,
|
||||
end: `${plus3DateString}T05:59:59.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
@@ -1347,12 +1350,15 @@ describe("getSchedule", () => {
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([
|
||||
{
|
||||
start: `${plus3DateString}T04:00:00.000Z`,
|
||||
end: `${plus3DateString}T05:59:59.000Z`,
|
||||
},
|
||||
]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
start: `${plus3DateString}T04:00:00.000Z`,
|
||||
end: `${plus3DateString}T05:59:59.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
@@ -1421,7 +1427,7 @@ describe("getSchedule", () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
|
||||
@@ -245,21 +245,11 @@ export const getBusyCalendarTimes = async (
|
||||
}
|
||||
|
||||
// const months = getMonths(dateFrom, dateTo);
|
||||
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||
const endDate = dayjs(dateTo).add(14, "hours").format();
|
||||
try {
|
||||
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||
const endDate = dayjs(dateTo).add(14, "hours").format();
|
||||
|
||||
log.debug(
|
||||
"getBusyCalendarTimes manipulated dates",
|
||||
safeStringify({
|
||||
newStartDate: startDate,
|
||||
newEndDate: endDate,
|
||||
oldStartDate: dateFrom,
|
||||
oldEndDate: dateTo,
|
||||
})
|
||||
);
|
||||
if (includeTimeZone) {
|
||||
results = await getCalendarsEventsWithTimezones(
|
||||
deduplicatedCredentials,
|
||||
@@ -281,8 +271,9 @@ export const getBusyCalendarTimes = async (
|
||||
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.externalId),
|
||||
error: safeStringify(e),
|
||||
});
|
||||
return { success: false, data: [{ start: startDate, end: endDate, source: "error-placeholder" }] };
|
||||
}
|
||||
return results.reduce((acc, availability) => acc.concat(availability), []);
|
||||
return { success: true, data: results.reduce((acc, availability) => acc.concat(availability), []) };
|
||||
};
|
||||
|
||||
export const createEvent = async (
|
||||
|
||||
@@ -179,13 +179,24 @@ const _getBusyTimes = async (params: {
|
||||
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
|
||||
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
|
||||
const startConnectedCalendarsGet = performance.now();
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
credentials,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new Error(
|
||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||
(calendar) => calendar.id
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
|
||||
@@ -411,24 +411,39 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id })
|
||||
: user.userLevelSelectedCalendars;
|
||||
|
||||
const busyTimes = await getBusyTimes({
|
||||
credentials: user.credentials,
|
||||
startTime: getBusyTimesStart,
|
||||
endTime: getBusyTimesEnd,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
username: `${user.username}`,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
||||
rescheduleUid: initialData?.rescheduleUid || null,
|
||||
duration,
|
||||
currentBookings: initialData?.currentBookings,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
let busyTimes = [];
|
||||
try {
|
||||
busyTimes = await getBusyTimes({
|
||||
credentials: user.credentials,
|
||||
startTime: getBusyTimesStart,
|
||||
endTime: getBusyTimesEnd,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
username: `${user.username}`,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
||||
rescheduleUid: initialData?.rescheduleUid || null,
|
||||
duration,
|
||||
currentBookings: initialData?.currentBookings,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error fetching busy times for user ${username}:`, error);
|
||||
return {
|
||||
busy: [],
|
||||
timeZone,
|
||||
dateRanges: [],
|
||||
oooExcludedDateRanges: [],
|
||||
workingHours: [],
|
||||
dateOverrides: [],
|
||||
currentSeats: [],
|
||||
datesOutOfOffice: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const detailedBusyTimes: EventBusyDetails[] = [
|
||||
...busyTimes.map((a) => ({
|
||||
|
||||
@@ -55,7 +55,7 @@ it("can find lucky user with maximize availability", async () => {
|
||||
}),
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
|
||||
// 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
|
||||
}),
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
@@ -289,7 +289,7 @@ describe("maximize availability and weights", () => {
|
||||
}),
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.host.findMany.mockResolvedValue([]);
|
||||
@@ -392,7 +392,7 @@ describe("maximize availability and weights", () => {
|
||||
}),
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.host.findMany.mockResolvedValue([]);
|
||||
@@ -500,7 +500,7 @@ describe("maximize availability and weights", () => {
|
||||
}),
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.host.findMany.mockResolvedValue([]);
|
||||
@@ -600,7 +600,7 @@ describe("maximize availability and weights", () => {
|
||||
},
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([
|
||||
{
|
||||
@@ -707,13 +707,16 @@ describe("maximize availability and weights", () => {
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
start: dayjs().utc().startOf("month").toDate(),
|
||||
end: dayjs().utc().startOf("month").add(3, "day").toDate(),
|
||||
timeZone: "UTC",
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
start: dayjs().utc().startOf("month").toDate(),
|
||||
end: dayjs().utc().startOf("month").add(3, "day").toDate(),
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValue([]);
|
||||
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
@@ -817,7 +820,7 @@ describe("maximize availability and weights", () => {
|
||||
},
|
||||
];
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
@@ -1279,7 +1282,7 @@ describe("attribute weights and virtual queues", () => {
|
||||
chosenRouteId: routeId,
|
||||
};
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
||||
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
|
||||
@@ -456,7 +456,7 @@ async function getCalendarBusyTimesOfInterval(
|
||||
rrTimestampBasis: RRTimestampBasis,
|
||||
meetingStartTime?: Date
|
||||
): Promise<{ userId: number; busyTimes: (EventBusyDate & { timeZone?: string })[] }[]> {
|
||||
return Promise.all(
|
||||
const usersBusyTimesQuery = await Promise.all(
|
||||
usersWithCredentials.map((user) =>
|
||||
getBusyCalendarTimes(
|
||||
user.credentials,
|
||||
@@ -465,12 +465,19 @@ async function getCalendarBusyTimesOfInterval(
|
||||
user.userLevelSelectedCalendars,
|
||||
true,
|
||||
true
|
||||
).then((busyTimes) => ({
|
||||
userId: user.id,
|
||||
busyTimes,
|
||||
}))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return usersBusyTimesQuery.reduce((usersBusyTime, userBusyTimeQuery, index) => {
|
||||
if (userBusyTimeQuery.success) {
|
||||
usersBusyTime.push({
|
||||
userId: usersWithCredentials[index].id,
|
||||
busyTimes: userBusyTimeQuery.data,
|
||||
});
|
||||
}
|
||||
return usersBusyTime;
|
||||
}, [] as { userId: number; busyTimes: Awaited<ReturnType<typeof getBusyCalendarTimes>>["data"] }[]);
|
||||
}
|
||||
|
||||
async function getBookingsOfInterval({
|
||||
|
||||
@@ -84,13 +84,22 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => {
|
||||
});
|
||||
|
||||
// get all clanedar services
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
credentials,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
composedSelectedCalendars
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to fetch busy calendar times",
|
||||
});
|
||||
}
|
||||
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
|
||||
// Convert to users timezone
|
||||
|
||||
const userTimeZone = input.loggedInUsersTz;
|
||||
|
||||
Reference in New Issue
Block a user